Table of Contents

Open MCT Tutorials

Victor Woeltjen victor.woeltjen@nasa.gov

October 14, 2015 Document Version 2.2

Date Version Summary of Changes Author
May 12, 2015 0 Initial Draft Victor Woeltjen
June 4, 2015 1.0 Name changes Victor Woeltjen
July 28, 2015 2.0 Telemetry adapter tutorial Victor Woeltjen
July 31, 2015 2.1 Clarify telemetry adapter details Victor Woeltjen
October 14, 2015 2.2 Conversion to markdown Andrew Henry

Introduction

This document

This document contains a number of code examples in formatted code blocks. In many cases these code blocks are repeated in order to highlight code that has been added or removed as part of the tutorial. In these cases, any lines added will be indicated with a '+' at the start of the line. Any lines removed will be indicated with a '-'.

Setting Up Open MCT

In this section, we will cover the steps necessary to get a minimal Open MCT developer environment up and running. Once we have this, we will be able to proceed with writing plugins as described in this tutorial.

Prerequisites

This tutorial assumes you have the following software installed. Version numbers record what was used in writing this tutorial; the same steps should work with more recent versions, but this cannot be guaranteed.

Open MCT can be run without any of these tools, provided suitable alternatives are taken; see the Open MCT Developer Guide for a more general overview of how to run and deploy a Open MCT application.

Check out Open MCT Sources

First step is to check out Open MCT from the source repository.

git clone https://github.com/nasa/openmct.git openmct

This will create a copy of the Open MCT source code repository in the folder openmct (relative to the path from which you ran the command.) If you have a repository URL, use that as the "path to repo" above. Alternately, if you received Open MCT as a git bundle, the path to that bundle on the local filesystem can be used instead. At this point, it will also be useful to branch off of Open MCT v0.6.2 (which was used when writing these tutorials) to begin adding plugins.

cd openmct
git branch <my branch name> open-v0.6.2
git checkout <my branch name>

Building Open MCT

Once downloaded, Open MCT can be built with the following command:

npm install

This will install various dependencies, build CSS from Sass files, run tests, and lint the source code.

It's not necessary to do this after every code change, unless you are making changes to stylesheets, or you are running the minified version of the app (under dist).

Run a Web Server

The next step is to run a web server so that you can view the Open MCT client (including the plugins you add to it) in browser. Any web server can be used for hosting Open MCT, and a trivial web server is provided in this package for the purposes of running the tutorials. The provided web server should not be used in a production environment

To run the tutorial web server

npm start

Viewing in Browser

Once running, you should be able to view Open MCT from your browser at http://localhost:8080/ (assuming the web server is running on port 8080, and Open MCT is installed at the server's root path). Google Chrome is recommended for these tutorials, as Chrome is Open MCT's "test-to" browser. The browser cache can sometimes interfere with development (masking changes by using older versions of sources); to avoid this, it is easiest to run Chrome with Developer Tools expanded, and "Disable cache" selected from the Network tab, as shown below.

Chrome Developer Tools

Tutorials

These tutorials cover three of the common tasks in Open MCT:

To-do List

The goal of this tutorial is to add a new application feature to Open MCT: To-do lists. Users should be able to create and manage these to track items that they need to do. This is modelled after the to-do lists at http://todomvc.com/.

Step 1-Create the Plugin

The first step to adding a new feature to Open MCT is to create the plugin which will expose that feature. A plugin in Open MCT is represented by what is called a bundle; a bundle, in turn, is a directory which contains a file bundle.js, which in turn describes where other relevant sources & resources will be. The syntax of this file is described in more detail in the Open MCT Developer Guide.

We will create this file in the directory tutorials/todo (we can hereafter refer to this plugin as tutorials/todo as well.) We will start with an "empty bundle", one which exposes no extensions - which looks like:

define([
    'openmct'
], function (
    openmct
) {
    openmct.legacyRegistry.register("tutorials/todo", {
        "name": "To-do Plugin",
        "description": "Allows creating and editing to-do lists.",
        "extensions":
        {
        }
    });
});

tutorials/todo/bundle.js

With the new bundle defined, it is now necessary to register the bundle with the application. The details of how a new bundle is defined are in the process of changing. The Open MCT codebase has started to shift from a declarative registration style toward an imperative registration style. The tutorials will be updated with the new bundle registration mechanism once it has been finalized.

Before

<!--
 Open MCT, Copyright (c) 2014-2016, United States Government
 as represented by the Administrator of the National Aeronautics and Space
 Administration. All rights reserved.

 Open MCT is licensed under the Apache License, Version 2.0 (the
 "License"); you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0.

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 License for the specific language governing permissions and limitations
 under the License.

 Open MCT includes source code licensed under additional open source
 licenses. See the Open Source Licenses file (LICENSES.md) included with
 this source code distribution or the Licensing information page available
 at runtime from the About dialog for additional information.
-->
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title></title>
    <script src="bower_components/requirejs/require.js">
    </script>
    <script>
        require([
            'openmct'
        ], function (openmct) {
            [
                'example/imagery',
                'example/eventGenerator',
                'example/generator',
                'platform/features/my-items',
                'platform/persistence/local'
            ].forEach(
                openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
            );
            openmct.start();
        });
    </script>
    <link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
    <link rel="stylesheet" href="platform/commonUI/general/res/css/openmct.css">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-32x32.png" sizes="32x32">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-96x96.png" sizes="96x96">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-16x16.png" sizes="16x16">
    <link rel="shortcut icon" href="platform/commonUI/general/res/images/favicons/favicon.ico">
</head>
<body class="user-environ">
    <div class="l-splash-holder s-splash-holder">
        <div class="l-splash s-splash"></div>
    </div>
</body>
</html>

index.html

After

<!--
 Open MCT, Copyright (c) 2014-2016, United States Government
 as represented by the Administrator of the National Aeronautics and Space
 Administration. All rights reserved.

 Open MCT is licensed under the Apache License, Version 2.0 (the
 "License"); you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0.

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 License for the specific language governing permissions and limitations
 under the License.

 Open MCT includes source code licensed under additional open source
 licenses. See the Open Source Licenses file (LICENSES.md) included with
 this source code distribution or the Licensing information page available
 at runtime from the About dialog for additional information.
-->
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title></title>
    <script src="bower_components/requirejs/require.js">
    </script>
    <script>
        require([
            'openmct',
+           'tutorials/todo/bundle'
        ], function (openmct) {
            [
                'example/imagery',
                'example/eventGenerator',
                'example/generator',
                'platform/features/my-items',
                'platform/persistence/local',
+               'tutorials/todo'
            ].forEach(
                openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
            );
            openmct.start();
        });
    </script>
    <link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
    <link rel="stylesheet" href="platform/commonUI/general/res/css/openmct.css">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-32x32.png" sizes="32x32">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-96x96.png" sizes="96x96">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-16x16.png" sizes="16x16">
    <link rel="shortcut icon" href="platform/commonUI/general/res/images/favicons/favicon.ico">
</head>
<body class="user-environ">
    <div class="l-splash-holder s-splash-holder">
        <div class="l-splash s-splash"></div>
    </div>
</body>
</html>

index.html

At this point, we can reload Open MCT. We haven't introduced any new functionality, so we don't see anything different, but if we run with logging enabled ( http://localhost:8080/?log=info ) and check the browser console, we should see:

Resolving extensions for bundle tutorials/todo(To-do Plugin)

...which shows that our plugin has loaded.

Step 2-Add a Domain Object Type

Features in a Open MCT application are most commonly expressed as domain objects and/or views thereof. A domain object is some thing that is relevant to the work that the Open MCT application is meant to support. Domain objects can be created, organized, edited, placed in layouts, and so forth. (For a deeper explanation of domain objects, see the Open MCT Developer Guide.)

In the case of our to-do list feature, the to-do list itself is the thing we'll want users to be able to create and edit. So, we will add that as a new type in our bundle definition:

define([
    'openmct'
], function (
    openmct
) {
    openmct.legacyRegistry.register("tutorials/todo", {
        "name": "To-do Plugin",
        "description": "Allows creating and editing to-do lists.",
        "extensions":
        {
+         "types": [
+          {
+              "key": "example.todo",
+              "name": "To-Do List",
+              "cssClass": "icon-check",
+              "description": "A list of things that need to be done.",
+              "features": ["creation"]
+          }
+       ]}
    });
});

tutorials/todo/bundle.js

What have we done here? We've stated that this bundle includes extensions of the category types, which is used to describe domain object types. Then, we've included a definition for one such extension, which is the to-do list object.

Going through the properties we've defined:

If we reload Open MCT, we see that our new domain object type appears in the Create menu:

To-Do List

At this point, our to-do list doesn't do much of anything; we can create them and give them names, but they don't have any specific functionality attached, because we haven't defined any yet.

Step 3-Add a View

In order to allow a to-do list to be used, we need to define and display its contents. In Open MCT, the pattern that the user expects is that they'll click on an object in the left-hand tree, and see a visualization of it to the right; in Open MCT, these visualizations are called views. A view in Open MCT is defined by an Angular template. We'll add that in the directory tutorials/todo/res/templates (res is, by default, the directory where bundle-related resources are kept, and templates is where HTML templates are stored by convention.)

<div>
    <a href="">All</a>
    <a href="">Incomplete</a>
    <a href="">Complete</a>
</div>

<ul>
    <li ng-repeat="task in model.tasks">
        <input type="checkbox" ng-checked="task.completed">
        {{task.description}}
    </li>
</ul>

tutorials/todo/res/templates/todo.html

A summary of what's included:

To expose this view in Open MCT, we need to declare it in our bundle definition:

define([
    'openmct'
], function (
    openmct
) {
    openmct.legacyRegistry.register("tutorials/todo", {
    "name": "To-do Plugin",
    "description": "Allows creating and editing to-do lists.",
    "extensions": {
        "types": [
            {
                "key": "example.todo",
                "name": "To-Do List",
                "cssClass": "icon-check",
                "description": "A list of things that need to be done.",
                "features": ["creation"]
            }
        ],
+       "views": [
+           {
+               "key": "example.todo",
+               "type": "example.todo",
+               "cssClass": "icon-check",
+               "name": "List",
+               "templateUrl": "templates/todo.html",
+               "editable": true
+           }
+       ]
    }
    });
});

tutorials/todo/bundle.js

Here, we've added another extension, this time belonging to category views. It contains the following properties:

This template looks like it should display tasks, but we don't have any way for the user to create these yet. As a temporary workaround to test the view, we will specify an initial state for To-do List domain object models in the definition of that type.

define([
    'openmct'
], function (
    openmct
) {
    openmct.legacyRegistry.register("tutorials/todo", {
    "name": "To-do Plugin",
    "description": "Allows creating and editing to-do lists.",
    "extensions": {
        "types": [
            {
                "key": "example.todo",
                "name": "To-Do List",
                "cssClass": "icon-check",
                "description": "A list of things that need to be done.",
                "features": ["creation"],
+               "model": {
+                   "tasks": [
+                       { "description": "Add a type", "completed": true },
+                       { "description": "Add a view" }
+                   ]
                }
            }
        ],
        "views": [
            {
                "key": "example.todo",
                "type": "example.todo",
                "cssClass": "icon-check",
                "name": "List",
                "templateUrl": "templates/todo.html",
                "editable": true
            }
        ]
    }
    });
});

tutorials/todo/bundle.js

Now, when To-do List objects are created in Open MCT, they will initially have the state described by that model property.

If we reload Open MCT, create a To-do List, and navigate to it in the tree, we should now see:

To-Do List

This looks roughly like what we want. We'll handle styling later, so let's work on adding functionality. Currently, the filter choices do nothing, and while the checkboxes can be checked/unchecked, we're not actually making the changes in the domain object - if we click over to My Items and come back to our To-Do List, for instance, we'll see that those check boxes have returned to their initial state.

Step 4-Add a Controller

We need to do some scripting to add dynamic behavior to that view. In particular, we want to:

To do this, we will support this by adding an Angular controller. (See https://docs.angularjs.org/guide/controller for an overview of controllers.) We will define that in an AMD module (see http://requirejs.org/docs/whyamd.html) in the directory tutorials/todo/src/controllers (src is, by default, the directory where bundle-related source code is kept, and controllers is where Angular controllers are stored by convention.)

define(function () {
    function TodoController($scope) {
        var showAll = true,
            showCompleted;

        // Persist changes made to a domain object's model
        function persist() {
            var persistence = 
                $scope.domainObject.getCapability('persistence');
            return persistence && persistence.persist();
        }

        // Change which tasks are visible
        $scope.setVisibility = function (all, completed) {
            showAll = all;
            showCompleted = completed;
        };

        // Toggle the completion state of a task
        $scope.toggleCompletion = function (taskIndex) {
            $scope.domainObject.useCapability('mutation', function (model) {
                var task = model.tasks[taskIndex];
                task.completed = !task.completed;
            });
            persist();
        };

        // Check whether a task should be visible
        $scope.showTask = function (task) {
            return showAll || (showCompleted === !!(task.completed));
        };
    }

    return TodoController;
});

tutorials/todo/src/controllers/TodoController.js

Here, we've defined three new functions and placed them in our $scope, which will make them available from the template:

Note that these functions make reference to $scope.domainObject; this is the domain object being viewed, which is passed into the scope by Open MCT prior to our template being utilized.

On its own, this controller merely exposes these functions; the next step is to use them from our template:

+  <div ng-controller="TodoController">
        <div>
+          <a ng-click="setVisibility(true)">All</a>
+          <a ng-click="setVisibility(false, false)">Incomplete</a>
+          <a ng-click="setVisibility(false, true)">Complete</a>
        </div>

        <ul>
            <li ng-repeat="task in model.tasks"
+              ng-if="showTask(task)">
               <input type="checkbox"
                      ng-checked="task.completed"
+                     ng-click="toggleCompletion($index)">
                {{task.description}}
            </li>
        </ul>
+ </div>

tutorials/todo/res/templates/todo.html

Summary of changes here:

If we were to try to run at this point, we'd run into problems because the TodoController has not been registered with Angular. We need to first declare it in our bundle definition, as an extension of category controllers:

define([
    'openmct',
+    './src/controllers/TodoController'
], function (
    openmct,
+    TodoController
) {
    openmct.legacyRegistry.register("tutorials/todo", {
    "name": "To-do Plugin",
    "description": "Allows creating and editing to-do lists.",
    "extensions": {
        "types": [
            {
                "key": "example.todo",
                "name": "To-Do List",
                "cssClass": "icon-check",
                "description": "A list of things that need to be done.",
                "features": ["creation"],
                "model": {
                    "tasks": [
                        { "description": "Add a type", "completed": true },
                        { "description": "Add a view" }
                    ]
                }
            }
        ],
        "views": [
            {
                "key": "example.todo",
                "type": "example.todo",
                "cssClass": "icon-check",
                "name": "List",
                "templateUrl": "templates/todo.html",
                "editable": true
            }
        ],
+       "controllers": [
+           {
+               "key": "TodoController",
+               "implementation": TodoController,
+               "depends": [ "$scope" ]
+           }
+       ]
    }
    });
});

tutorials/todo/bundle.js

In this extension definition we have:

If we reload the browser now, our To-do List looks much the same, but now we are able to filter down the visible list, and the changes we make will stick around if we go to My Items and come back.

Step 5-Support Editing

We now have a somewhat-functional view of our To-Do List, but we're still missing some important functionality: Adding and removing tasks!

This is a good place to discuss the user interface style of Open MCT. Open MCT Web draws a distinction between "using" and "editing" a domain object; in general, you can only make changes to a domain object while in Edit mode, which is reachable from the button with a pencil icon. This distinction helps users keep these tasks separate.

The distinction between "using" and "editing" may vary depending on what domain objects or views are being used. While it may be convenient for a developer to think of "editing" as "any changes made to a domain object," in practice some of these activities will be thought of as "using."

For this tutorial we'll consider checking/unchecking tasks as "using" To-Do Lists, and adding/removing tasks as "editing." We've already implemented the "using" part, in this case, so let's focus on editing.

There are two new pieces of functionality we'll want out of this step:

An Editing user interface is typically handled in a tool bar associated with a view. The contents of this tool bar are defined declaratively in a view's extension definition.

define([
    'openmct',
    './src/controllers/TodoController'
], function (
    openmct,
    TodoController
) {
    openmct.legacyRegistry.register("tutorials/todo", {
    "name": "To-do Plugin",
    "description": "Allows creating and editing to-do lists.",
    "extensions": {
        "types": [
            {
                "key": "example.todo",
                "name": "To-Do List",
                "cssClass": "icon-check",
                "description": "A list of things that need to be done.",
                "features": ["creation"],
                "model": {
                    "tasks": [
                        { "description": "Add a type", "completed": true },
                        { "description": "Add a view" }
                    ]
                }
            }
        ],
        "views": [
            {
                "key": "example.todo",
                "type": "example.todo",
                "cssClass": "icon-check",
                "name": "List",
                "templateUrl": "templates/todo.html",
                "editable": true,
+               "toolbar": {
+                   "sections": [
+                       {
+                           "items": [
+                               {
+                                   "text": "Add Task",
+                                   "cssClass": "icon-plus",
+                                   "method": "addTask",
+                                   "control": "button"
+                               }
+                           ]
+                       },
+                       {
+                           "items": [
+                               {
+                                   "cssClass": "icon-trash",
+                                   "method": "removeTask",
+                                   "control": "button"
+                               }
+                           ]
+                       }
+                   ]
+               }
            }
        ],
        "controllers": [
            {
                "key": "TodoController",
                "implementation": TodoController,
                "depends": [ "$scope" ]
            }
        ]
    }
    });
});

tutorials/todo/bundle.js

What we've stated here is that the To-Do List's view will have a toolbar which contains two sections (which will be visually separated by a divider), each of which contains one button. The first is a button labelled "Add Task" that will invoke an addTask method; the second is a button with a glyph (which will appear as a trash can in Open MCT's custom font set) which will invoke a removeTask method. For more information on forms and tool bars in Open MCT, see the Open MCT Developer Guide.

If we reload and run Open MCT, we won't see any tool bar when we switch over to Edit mode. This is because the aforementioned methods are expected to be found on currently-selected elements; we haven't done anything with selections in our view yet, so the Open MCT platform will filter this tool bar down to all the applicable controls, which means no controls at all.

To support selection, we will need to make some changes to our controller:

define(function () {
+    // Form to display when adding new tasks
+    var NEW_TASK_FORM = {
+        name: "Add a Task",
+        sections: [{
+            rows: [{
+                name: 'Description',
+                key: 'description',
+                control: 'textfield',
+                required: true
+            }]
+        }]
+    };

+   function TodoController($scope, dialogService) {
        var showAll = true,
            showCompleted;

        // Persist changes made to a domain object's model
        function persist() {
            var persistence = 
                $scope.domainObject.getCapability('persistence');
            return persistence && persistence.persist();
        }

+       // Remove a task
+       function removeTaskAtIndex(taskIndex) {
+           $scope.domainObject.useCapability('mutation', function 
+       (model) {
+               model.tasks.splice(taskIndex, 1);
+           });
+           persist();
+       }

+       // Add a task
+       function addNewTask(task) {
+           $scope.domainObject.useCapability('mutation', function 
+           (model) {
+               model.tasks.push(task);
+           });
+           persist();
+       }

        // Change which tasks are visible
        $scope.setVisibility = function (all, completed) {
            showAll = all;
            showCompleted = completed;
        };

        // Toggle the completion state of a task
        $scope.toggleCompletion = function (taskIndex) {
            $scope.domainObject.useCapability('mutation', function (model) {
                var task = model.tasks[taskIndex];
                task.completed = !task.completed;
            });
            persist();
        };

        // Check whether a task should be visible
        $scope.showTask = function (task) {
            return showAll || (showCompleted === !!(task.completed));
        };

        // Handle selection state in edit mode
+       if ($scope.selection) {
+           // Expose the ability to select tasks
+           $scope.selectTask = function (taskIndex) {
+               $scope.selection.select({
+                   removeTask: function () {
+                       removeTaskAtIndex(taskIndex);
+                       $scope.selection.deselect();
+                   }
+               });
+           };

+           // Expose a view-level selection proxy
+           $scope.selection.proxy({
+               addTask: function () {
+                   dialogService.getUserInput(NEW_TASK_FORM, {})
+                       .then(addNewTask);
+               }
+           });
+       }
    }

    return TodoController;
});

tutorials/todo/src/controllers/TodoController.js

There are a few changes to pay attention to here. Let's review them:

Additionally, we need to make changes to our template to select specific tasks in response to some user gesture. Here, we will select tasks when a user clicks the description.

<div ng-controller="TodoController">
    <div>
        <a ng-click="setVisibility(true)">All</a>
        <a ng-click="setVisibility(false, false)">Incomplete</a>
        <a ng-click="setVisibility(false, true)">Complete</a>
    </div>

    <ul>
        <li ng-repeat="task in model.tasks"
            ng-if="showTask(task)">
            <input type="checkbox"
                   ng-checked="task.completed"
                   ng-click="toggleCompletion($index)">
+           <span ng-click="selectTask($index)">
                {{task.description}}
+           </span>
        </li>
    </ul>
</div>

tutorials/todo/res/templates/todo.html

Finally, the TodoController uses the dialogService now, so we need to declare that dependency in its extension definition:

define([
    'openmct',
    './src/controllers/TodoController'
], function (
    openmct,
    TodoController
) {
    openmct.legacyRegistry.register("tutorials/todo", {
    "name": "To-do Plugin",
    "description": "Allows creating and editing to-do lists.",
    "extensions": {
        "types": [
            {
                "key": "example.todo",
                "name": "To-Do List",
                "cssClass": "icon-check",
                "description": "A list of things that need to be done.",
                "features": ["creation"],
                "model": {
                    "tasks": [
                        { "description": "Add a type", "completed": true },
                        { "description": "Add a view" }
                    ]
                }
            }
        ],
        "views": [
            {
                "key": "example.todo",
                "type": "example.todo",
                "cssClass": "icon-check",
                "name": "List",
                "templateUrl": "templates/todo.html",
                "editable": true,
                "toolbar": {
                    "sections": [
                        {
                            "items": [
                                {
                                    "text": "Add Task",
                                    "cssClass": "icon-plus",
                                    "method": "addTask",
                                    "control": "button"
                                }
                            ]
                        },
                        {
                            "items": [
                                {
                                    "cssClass": "icon-trash",
                                    "method": "removeTask",
                                    "control": "button"
                                }
                            ]
                        }
                    ]
                }
            }
        ],
        "controllers": [
            {
                "key": "TodoController",
                "implementation": TodoController,
+               "depends": [ "$scope", "dialogService" ]
            }
        ]
    }
    });
});

tutorials/todo/bundle.js

If we now reload Open MCT, we'll be able to see the new functionality we've added. If we Create a new To-Do List, navigate to it, and click the button with the Pencil icon in the top-right, we'll be in edit mode. We see, first, that our "Add Task" button appears in the tool bar:

Edit

If we click on this, we'll get a dialog allowing us to add a new task:

Add task

Finally, if we click on the description of a specific task, we'll see a new button appear, which we can then click on to remove that task:

Remove task

As always in Edit mode, the user will be able to Save or Cancel any changes they have made. In terms of functionality, our To-Do List can do all the things we want, but the appearance is still lacking. In particular, we can't distinguish our current filter choice or our current selection state.

Step 6-Customizing Look and Feel

In this section, our goal is to:

To support the first two, we'll need to expose some methods for checking these states in the controller:

define(function () {
    // Form to display when adding new tasks
    var NEW_TASK_FORM = {
        name: "Add a Task",
        sections: [{
            rows: [{
                name: 'Description',
                key: 'description',
                control: 'textfield',
                required: true
            }]
        }]
    };

    function TodoController($scope, dialogService) {
        var showAll = true,
            showCompleted;

        // Persist changes made to a domain object's model
        function persist() {
            var persistence = 
                $scope.domainObject.getCapability('persistence');
            return persistence && persistence.persist();
        }

        // Remove a task
        function removeTaskAtIndex(taskIndex) {
            $scope.domainObject.useCapability('mutation', function (model) {
                model.tasks.splice(taskIndex, 1);
            });
            persist();
        }

        // Add a task
        function addNewTask(task) {
            $scope.domainObject.useCapability('mutation', function (model) {
                model.tasks.push(task);
            });
            persist();
        }

        // Change which tasks are visible
        $scope.setVisibility = function (all, completed) {
            showAll = all;
            showCompleted = completed;
        };

+       // Check if current visibility settings match
+       $scope.checkVisibility = function (all, completed) {
+           return showAll ? all : (completed === showCompleted);
+       };

        // Toggle the completion state of a task
        $scope.toggleCompletion = function (taskIndex) {
            $scope.domainObject.useCapability('mutation', function (model) {
                var task = model.tasks[taskIndex];
                task.completed = !task.completed;
            });
            persist();
        };

        // Check whether a task should be visible
        $scope.showTask = function (task) {
            return showAll || (showCompleted === !!(task.completed));
        };

        // Handle selection state in edit mode
        if ($scope.selection) {
            // Expose the ability to select tasks
            $scope.selectTask = function (taskIndex) {
                $scope.selection.select({
                    removeTask: function () {
                        removeTaskAtIndex(taskIndex);
                        $scope.selection.deselect();
                    },
+                   taskIndex: taskIndex
                });
            };

+           // Expose a check for current selection state
+           $scope.isSelected = function (taskIndex) {
+               return ($scope.selection.get() || {}).taskIndex === 
+               taskIndex;
+           };

            // Expose a view-level selection proxy
            $scope.selection.proxy({
                addTask: function () {
                    dialogService.getUserInput(NEW_TASK_FORM, {})
                        .then(addNewTask);
                }
            });
        }
    }

    return TodoController;
});

tutorials/todo/src/controllers/TodoController.js

A summary of these changes:

Additionally, we will want to define some CSS rules in order to reflect these states visually, and to generally improve the appearance of our view. We add another file to the res directory of our bundle; this time, it is css/todo.css (with the css directory again being a convention.)

.example-todo div.example-button-group {
    margin-top: 12px;
    margin-bottom: 12px;
}

.example-todo .example-button-group a {
    padding: 3px;
    margin: 3px;
}

.example-todo .example-button-group a.selected {
    border: 1px gray solid;
    border-radius: 3px;
    background: #444;
}

.example-todo .example-task-completed .example-task-description {
    text-decoration: line-through;
    opacity: 0.75;
}

.example-todo .example-task-description.selected {
    background: #46A;
    border-radius: 3px;
}

.example-todo .example-message {
    font-style: italic;
}

tutorials/todo/res/css/todo.css

Here, we have defined classes and appearances for:

To include this CSS file in our running instance of Open MCT, we need to declare it in our bundle definition, this time as an extension of category stylesheets:

define([
    'openmct',
    './src/controllers/TodoController'
], function (
    openmct,
    TodoController
) {
    openmct.legacyRegistry.register("tutorials/todo", {
    "name": "To-do Plugin",
    "description": "Allows creating and editing to-do lists.",
    "extensions": {
        "types": [
            {
                "key": "example.todo",
                "name": "To-Do List",
                "cssClass": "icon-check",
                "description": "A list of things that need to be done.",
                "features": ["creation"],
                "model": {
                    "tasks": []
                }
            }
        ],
        "views": [
            {
                "key": "example.todo",
                "type": "example.todo",
                "cssClass": "icon-check",
                "name": "List",
                "templateUrl": "templates/todo.html",
                "editable": true,
                "toolbar": {
                    "sections": [
                        {
                            "items": [
                                {
                                    "text": "Add Task",
                                    "cssClass": "icon-plus",
                                    "method": "addTask",
                                    "control": "button"
                                }
                            ]
                        },
                        {
                            "items": [
                                {
                                    "cssClass": "icon-trash",
                                    "method": "removeTask",
                                    "control": "button"
                                }
                            ]
                        }
                    ]
                }
            }
        ],
        "controllers": [
            {
                "key": "TodoController",
                "implementation": TodoController,
                "depends": [ "$scope", "dialogService" ]
            }
        ],
+       "stylesheets": [
+           {
+               "stylesheetUrl": "css/todo.css"
+           }
+       ]
    }
    });
});

tutorials/todo/bundle.js

Note that we've also removed our placeholder tasks from the model of the To-Do List's type above; now To-Do Lists will start off empty.

Finally, let's utilize these changes from our view's template:

+ <div ng-controller="TodoController" class="example-todo">
+     <div class="example-button-group">
+         <a ng-class="{ selected: checkVisibility(true) }"
             ng-click="setVisibility(true)">All</a>
+         <a ng-class="{ selected: checkVisibility(false, false) }"
             ng-click="setVisibility(false, false)">Incomplete</a>
+         <a ng-class="{ selected: checkVisibility(false, true) }"
             ng-click="setVisibility(false, true)">Complete</a>
      </div>

      <ul>
          <li ng-repeat="task in model.tasks"
+             ng-class="{ 'example-task-completed': task.completed }"
              ng-if="showTask(task)">
              <input type="checkbox"
                   ng-checked="task.completed"
                   ng-click="toggleCompletion($index)">
              <span ng-click="selectTask($index)"
+                 ng-class="{ selected: isSelected($index) }"
+                 class="example-task-description">
                {{task.description}}
              </span>
          </li>
      </ul>
+     <div ng-if="model.tasks.length < 1" class="example-message">
+          There are no tasks to show.
+     </div>
+ </div>

tutorials/todo/res/templates/todo.html

Now, if we reload our page and create a new To-Do List, we will initially see:

Todo Restyled

If we then go into Edit mode, add some tasks, and select one, it will now be much clearer what the current selection is (e.g. before we hit the remove button in the toolbar):

Todo Restyled

Bar Graph

In this tutorial, we will look at creating a bar graph plugin for visualizing telemetry data. Specifically, we want some bars that raise and lower to match the observed state of real-time telemetry; this is particularly useful for monitoring things like battery charge levels. It is recommended that the reader completes (or is familiar with) the To-Do List tutorial before completing this tutorial, as certain concepts discussed there will be addressed in more brevity here.

Step 1-Define the View

Since the goal is to introduce a new view and expose it from a plugin, we will want to create a new bundle which declares an extension of category views. We'll also be defining some custom styles, so we'll include that extension as well. We'll be creating this plugin in tutorials/bargraph, so our initial bundle definition looks like:

define([
    'openmct'
], function (
    openmct
) {
    openmct.legacyRegistry.register("tutorials/bargraph", {
    "name": "Bar Graph",
    "description": "Provides the Bar Graph view of telemetry elements.",
    "extensions": {
        "views": [
            {
                "name": "Bar Graph",
                "key": "example.bargraph",
                "cssClass": "icon-autoflow-tabular",
                "templateUrl": "templates/bargraph.html",
                "needs": [ "telemetry" ],
                "delegation": true
            }
        ],
        "stylesheets": [
            {
                "stylesheetUrl": "css/bargraph.css"
            }
        ]
    }
    });
});

tutorials/bargraph/bundle.js

The view definition should look familiar after the To-Do List tutorial, with some additions:

For this tutorial, we'll assume that we've sketched out our template and CSS file ahead of time to describe the general look we want for the view. These look like:

<div class="example-bargraph">
    <div class="example-tick-labels">
        <div class="example-tick-label" style="bottom: 0%">High</div>
        <div class="example-tick-label" style="bottom: 50%">Middle</div>
        <div class="example-tick-label" style="bottom: 100%">Low</div>
    </div>

    <div class="example-graph-area">
        <div style="left: 0; width: 33.3%;" class="example-bar-holder">
            <div class="example-bar" style="top: 25%; bottom: 50%;">
            </div>
        </div>
        <div style="left: 33.3%; width: 33.3%;" class="example-bar-holder">
            <div class="example-bar" style="top: 40%; bottom: 10%;">
            </div>
        </div>
        <div style="left: 66.6%; width: 33.3%;" class="example-bar-holder">
            <div class="example-bar" style="top: 30%; bottom: 40%;">
            </div>
        </div>
        <div style="bottom: 50%" class="example-graph-tick">
        </div>
    </div>

    <div class="example-bar-labels">
        <div style="left: 0; width: 33.3%;" 
             class="example-bar-holder example-label">
            Label A
        </div>
        <div style="left: 33.3%; width: 33.3%;" 
             class="example-bar-holder example-label">
            Label B
        </div>
        <div style="left: 66.6%; width: 33.3%;" 
             class="example-bar-holder example-label">
            Label C
        </div>
    </div>
</div>

tutorials/bargraph/res/templates/bargraph.html

Here, three regions are defined. The first will be for tick labels along the vertical axis, showing the numeric value that certain heights correspond to. The second will be for the actual bar graphs themselves; three are included here. The third is for labels along the horizontal axis, which will indicate which bar corresponds to which telemetry point. Inline style attributes are used wherever dynamic positioning (handled by a script) is anticipated. The corresponding CSS file which styles and positions these elements:

.example-bargraph {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    mid-width: 160px;
    min-height: 160px;
}

.example-bargraph .example-tick-labels {
    position: absolute;
    left: 0;
    top: 24px;
    bottom: 32px;
    width: 72px;
    font-size: 75%;
}

.example-bargraph .example-tick-label {
    position: absolute;
    right: 0;
    height: 1em;
    margin-bottom: -0.5em;
    padding-right: 6px;
    text-align: right;
}

.example-bargraph .example-graph-area {
    position: absolute;
    border: 1px gray solid;
    left: 72px;
    top: 24px;
    bottom: 32px;
    right: 0;
}

.example-bargraph .example-bar-labels {
    position: absolute;
    left: 72px;
    bottom: 0;
    right: 0;
    height: 32px;
}

.example-bargraph .example-bar-holder {
    position: absolute;
    top: 0;
    bottom: 0;
}

.example-bargraph .example-graph-tick {
    position: absolute;
    width: 100%;
    height: 1px;
    border-bottom: 1px gray dashed;
}

.example-bargraph .example-bar {
    position: absolute;
    background: darkcyan;
    right: 4px;
    left: 4px;
}

.example-bargraph .example-label {
    text-align: center;
    font-size: 85%;
    padding-top: 6px;
}

tutorials/bargraph/res/css/bargraph.css

This is already enough that, if we add "tutorials/bargraph" to index.html, we should be able to run Open MCT and see our Bar Graph as an available view for domain objects which provide telemetry (such as the example Sine Wave Generator) as well as for Telemetry Panel objects:

Bar Plot

This means that our remaining work will be to populate and position these elements based on the actual contents of the domain object.

Step 2-Add a Controller

Our next step will be to begin dynamically populating this template's contents. Specifically, our goals for this step will be to:

Notably, we will not try to show telemetry data after this step.

To support this, we will add a new controller which supports our Bar Graph view:

define(function () {
    function BarGraphController($scope, telemetryHandler) {
        var handle;

        // Add min/max defaults
        $scope.low = -1;
        $scope.middle = 0;
        $scope.high = 1;

        // Convert value to a percent between 0-100, keeping values in points
        $scope.toPercent = function (value) {
            var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low);
            return Math.min(100, Math.max(0, pct));
        };

        // Use the telemetryHandler to get telemetry objects here
        handle = telemetryHandler.handle($scope.domainObject, function () {
            $scope.telemetryObjects = handle.getTelemetryObjects();
            $scope.barWidth = 
                100 / Math.max(($scope.telemetryObjects).length, 1);
        });

        // Release subscriptions when scope is destroyed
        $scope.$on('$destroy', handle.unsubscribe);
    }

    return BarGraphController;
});

tutorials/bargraph/src/controllers/BarGraphController.js

A summary of what we've done here:

Whenever the telemetry handler invokes its callbacks, we update the set of telemetry objects in view, as well as the width for each bar.

We will also utilize this from our template:

+ <div class="example-bargraph" ng-controller="BarGraphController">
    <div class="example-tick-labels">
+       <div ng-repeat="value in [low, middle, high] track by $index"
+             class="example-tick-label"
+             style="bottom: {{ toPercent(value) }}%">
+            {{value}}
+       </div>
    </div>

    <div class="example-graph-area">
+       <div ng-repeat="telemetryObject in telemetryObjects"
+           style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
+           class="example-bar-holder">
            <div class="example-bar"
                style="top: 25%; bottom: 50%;">
            </div>
+        </div>
+        <div style="bottom: {{ toPercent(middle) }}%"
             class="example-graph-tick">
         </div>
    </div>

    <div class="example-bar-labels">
+       <div ng-repeat="telemetryObject in telemetryObjects"
+            style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
+            class="example-bar-holder example-label">
+           <mct-representation key="'label'"
+                               mct-object="telemetryObject">
+           </mct-representation>
+       </div>
    </div>
</div>

tutorials/bargraph/res/templates/bargraph.html

Summarizing these changes:

Finally, we expose our controller from our bundle definition. Note that the depends declaration includes both $scope as well as the telemetryHandler service we made use of.

define([
    'openmct',
    './src/controllers/BarGraphController'
], function (
    openmct,
    BarGraphController
) {
    openmct.legacyRegistry.register("tutorials/bargraph", {
    "name": "Bar Graph",
    "description": "Provides the Bar Graph view of telemetry elements.",
    "extensions": {
        "views": [
            {
                "name": "Bar Graph",
                "key": "example.bargraph",
                "cssClass": "icon-autoflow-tabular",
                "templateUrl": "templates/bargraph.html",
                "needs": [ "telemetry" ],
                "delegation": true
            }
        ],
        "stylesheets": [
            {
                "stylesheetUrl": "css/bargraph.css"
            }
        ],
+       "controllers": [
+           {
+               "key": "BarGraphController",
+               "implementation": BarGraphController,
+               "depends": [ "$scope", "telemetryHandler" ]
+           }
+       ]
    }
    });
});

tutorials/bargraph/bundle.js

When we reload Open MCT, we are now able to see that our bar graph view correctly labels one bar per telemetry-providing domain object, as shown for this Telemetry Panel containing four Sine Wave Generators.

Bar Plot

Step 3-Using Telemetry Data

Now that our bar graph is labeled correctly, it's time to start putting data into the view.

First, let's add expose some more functionality from our controller. To make it simple, we'll expose the top and bottom for a bar graph for a given telemetry-providing domain object, as percentages.

define(function () {
    function BarGraphController($scope, telemetryHandler) {
        var handle;

        // Add min/max defaults
        $scope.low = -1;
        $scope.middle = 0;
        $scope.high = 1;

        // Convert value to a percent between 0-100, keeping values in points
        $scope.toPercent = function (value) {
            var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low);
            return Math.min(100, Math.max(0, pct));
        };

        // Get bottom and top (as percentages) for current value
+       $scope.getBottom = function (telemetryObject) {
+           var value = handle.getRangeValue(telemetryObject);
+           return $scope.toPercent(Math.min($scope.middle, value));
+       }
+       $scope.getTop = function (telemetryObject) {
+           var value = handle.getRangeValue(telemetryObject);
+           return 100 - $scope.toPercent(Math.max($scope.middle, value));
+       }        

        // Use the telemetryHandler to get telemetry objects here
        handle = telemetryHandler.handle($scope.domainObject, function () {
            $scope.telemetryObjects = handle.getTelemetryObjects();
            $scope.barWidth = 
                100 / Math.max(($scope.telemetryObjects).length, 1);
        });

        // Release subscriptions when scope is destroyed
        $scope.$on('$destroy', handle.unsubscribe);
    }

    return BarGraphController;
});

tutorials/bargraph/src/controllers/BarGraphController.js

The telemetryHandler exposes a method to provide us with our latest data value (the getRangeValue method), and we already have a function to convert from a numeric value to a percentage within the view, so we just use those. The only slight complication is that we want our bar to move up or down from the middle value, so either of our top or bottom position for the bar itself could be either the middle line, or the data value. We let Math.min and Math.max decide this.

Next, we utilize this functionality from the template:

<div class="example-bargraph" ng-controller="BarGraphController">
    <div class="example-tick-labels">
        <div ng-repeat="value in [low, middle, high] track by $index"
             class="example-tick-label"
             style="bottom: {{ toPercent(value) }}%">
            {{value}}
        </div>
    </div>

    <div class="example-graph-area">
        <div ng-repeat="telemetryObject in telemetryObjects"
             style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
             class="example-bar-holder">
            <div class="example-bar"
+                ng-style="{
+                    bottom: getBottom(telemetryObject) + '%',
+                    top: getTop(telemetryObject) + '%'
+                }">
            </div>
        </div>
        <div style="bottom: {{ toPercent(middle) }}%"
             class="example-graph-tick">
        </div>
    </div>

    <div class="example-bar-labels">
        <div ng-repeat="telemetryObject in telemetryObjects"
             style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
             class="example-bar-holder example-label">
            <mct-representation key="'label'"
                                mct-object="telemetryObject">
            </mct-representation>
        </div>
    </div>
</div>

tutorials/bargraph/res/templates/bargraph.html

Here, we utilize the functions we just provided from the controller to position the bar, using an ng-style attribute.

When we reload Open MCT, our bar graph view now looks like:

Bar Plot

Step 4-View Configuration

The default minimum and maximum values we've provided happen to make sense for sine waves, but what about other values? We want to provide the user with a means of configuring these boundaries.

This is normally done via Edit mode. Since view configuration is a common problem, the Open MCT platform exposes a configuration object - called configuration - into our view's scope. We can populate it as we please, and when we return to our view later, those changes will be persisted.

First, let's add a tool bar for changing these three values in Edit mode:

define([
    'openmct',
    './src/controllers/BarGraphController'
], function (
    openmct,
    BarGraphController
) {
    openmct.legacyRegistry.register("tutorials/bargraph", {
    "name": "Bar Graph",
    "description": "Provides the Bar Graph view of telemetry elements.",
    "extensions": {
        "views": [
            {
                "name": "Bar Graph",
                "key": "example.bargraph",
                "cssClass": "icon-autoflow-tabular",
                "templateUrl": "templates/bargraph.html",
                "needs": [ "telemetry" ],
                "delegation": true,
+               "toolbar": {
+                   "sections": [
+                       {
+                           "items": [
+                               {
+                                   "name": "Low",
+                                   "property": "low",
+                                   "required": true,
+                                   "control": "textfield",
+                                   "size": 4
+                               },
+                               {
+                                   "name": "Middle",
+                                   "property": "middle",
+                                   "required": true,
+                                   "control": "textfield",
+                                   "size": 4
+                               },
+                               {
+                                   "name": "High",
+                                   "property": "high",
+                                   "required": true,
+                                   "control": "textfield",
+                                   "size": 4
+                               }
+                           ]
+                       }
                    ]
                }
            }
        ],
        "stylesheets": [
            {
                "stylesheetUrl": "css/bargraph.css"
            }
        ],
        "controllers": [
            {
                "key": "BarGraphController",
                "implementation": BarGraphController,
                "depends": [ "$scope", "telemetryHandler" ]
            }
        ]
    }
    });
});

tutorials/bargraph/bundle.js

As we saw in to To-Do List plugin, a tool bar needs either a selected object or a view proxy to work from. We will add this to our controller, and additionally will start reading/writing those properties to the view's configuration object.

define(function () {
    function BarGraphController($scope, telemetryHandler) {
        var handle;

+       // Expose configuration constants directly in scope
+       function exposeConfiguration() {
+           $scope.low = $scope.configuration.low;
+           $scope.middle = $scope.configuration.middle;
+           $scope.high = $scope.configuration.high;
+       }

+       // Populate a default value in the configuration
+       function setDefault(key, value) {
+           if ($scope.configuration[key] === undefined) {
+               $scope.configuration[key] = value;
+           }
+       }

+       // Getter-setter for configuration properties (for view proxy)
+       function getterSetter(property) {
+           return function (value) {
+               value = parseFloat(value);
+               if (!isNaN(value)) {
+                   $scope.configuration[property] = value;
+                   exposeConfiguration();
+               }
+               return $scope.configuration[property];
+           };
        }

+       // Add min/max defaults
+       setDefault('low', -1);
+       setDefault('middle', 0);
+       setDefault('high', 1);
+       exposeConfiguration($scope.configuration);

+       // Expose view configuration options
+       if ($scope.selection) {
+           $scope.selection.proxy({
+               low: getterSetter('low'),
+               middle: getterSetter('middle'),
+               high: getterSetter('high')
+           });
+       }

        // Convert value to a percent between 0-100
        $scope.toPercent = function (value) {
            var pct = 100 * (value - $scope.low) / 
                ($scope.high - $scope.low);
            return Math.min(100, Math.max(0, pct));
        };

        // Get bottom and top (as percentages) for current value
        $scope.getBottom = function (telemetryObject) {
            var value = handle.getRangeValue(telemetryObject);
            return $scope.toPercent(Math.min($scope.middle, value));
        }
        $scope.getTop = function (telemetryObject) {
            var value = handle.getRangeValue(telemetryObject);
            return 100 - $scope.toPercent(Math.max($scope.middle, value));
        }        

        // Use the telemetryHandler to get telemetry objects here
        handle = telemetryHandler.handle($scope.domainObject, function () {
            $scope.telemetryObjects = handle.getTelemetryObjects();
            $scope.barWidth = 
                100 / Math.max(($scope.telemetryObjects).length, 1);
        });

        // Release subscriptions when scope is destroyed
        $scope.$on('$destroy', handle.unsubscribe);
    }

    return BarGraphController;
});

tutorials/bargraph/src/controllers/BarGraphController.js

A summary of these changes:

If we reload Open MCT and go to a Bar Graph view in Edit mode, we now see that we can change these bounds from the tool bar.

Bar plot

Telemetry Adapter

The goal of this tutorial is to demonstrate how to integrate Open MCT with an existing telemetry system.

A summary of the steps we will take:

Step 0-Expose Your Telemetry

As a precondition to integrating telemetry data into Open MCT, this information needs to be available over web-based interfaces. In practice, this will most likely mean exposing data over HTTP, or over WebSockets. For purposes of this tutorial, a simple node server is provided to stand in place of this existing telemetry system. It generates real-time data and exposes it over a WebSocket connection.

/*global require,process,console*/

var CONFIG = {
    port: 8081,
    dictionary: "dictionary.json",
    interval: 1000
};

(function () {
    "use strict";

    var WebSocketServer = require('ws').Server,
        fs = require('fs'),
        wss = new WebSocketServer({ port: CONFIG.port }),
        dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")),
        spacecraft = {
            "prop.fuel": 77,
            "prop.thrusters": "OFF",
            "comms.recd": 0,
            "comms.sent": 0,
            "pwr.temp": 245,
            "pwr.c": 8.15,
            "pwr.v": 30
        },
        histories = {},
        listeners = [];

    function updateSpacecraft() {
        spacecraft["prop.fuel"] = Math.max(
            0,
            spacecraft["prop.fuel"] -
                (spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0)
        );
        spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985
            + Math.random() * 0.25 + Math.sin(Date.now());
        spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985;
        spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3);
    }

    function generateTelemetry() {
        var timestamp = Date.now(), sent = 0;
        Object.keys(spacecraft).forEach(function (id) {
            var state = { timestamp: timestamp, value: spacecraft[id] };
            histories[id] = histories[id] || []; // Initialize
            histories[id].push(state);
            spacecraft["comms.sent"] += JSON.stringify(state).length;
        });
        listeners.forEach(function (listener) {
            listener();
        });
    }

    function update() {
        updateSpacecraft();
        generateTelemetry();
    }

    function handleConnection(ws) {
        var subscriptions = {}, // Active subscriptions for this connection
            handlers = {        // Handlers for specific requests
                dictionary: function () {
                    ws.send(JSON.stringify({
                        type: "dictionary",
                        value: dictionary
                    }));
                },
                subscribe: function (id) {
                    subscriptions[id] = true;
                },
                unsubscribe: function (id) {
                    delete subscriptions[id];
                },
                history: function (id) {
                    ws.send(JSON.stringify({
                        type: "history",
                        id: id,
                        value: histories[id]
                    }));
                }
            };

        function notifySubscribers() {
            Object.keys(subscriptions).forEach(function (id) {
                var history = histories[id];
                if (history) {
                    ws.send(JSON.stringify({
                        type: "data",
                        id: id,
                        value: history[history.length - 1]
                    }));
                }
            });
        }

        // Listen for requests
        ws.on('message', function (message) {
            var parts = message.split(' '),
                handler = handlers[parts[0]];
            if (handler) {
                handler.apply(handlers, parts.slice(1));
            }
        });

        // Stop sending telemetry updates for this connection when closed
        ws.on('close', function () {
            listeners = listeners.filter(function (listener) {
                return listener !== notifySubscribers;
            });
        });

        // Notify subscribers when telemetry is updated
        listeners.push(notifySubscribers);
    }

    update();
    setInterval(update, CONFIG.interval);

    wss.on('connection', handleConnection);

    console.log("Example spacecraft running on port ");
    console.log("Press Enter to toggle thruster state.");
    process.stdin.on('data', function (data) {
        spacecraft['prop.thrusters'] =
            (spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF";
        console.log("Thrusters " + spacecraft["prop.thrusters"]);
    });
}());

tutorial-server/app.js

For purposes of this tutorial, how this server has been implemented is not important; it has just enough functionality to resemble a WebSocket interface to a real telemetry system, and niceties such as error-handling have been omitted. (For more information on using WebSockets, both in the client and on the server, https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API is an excellent starting point.)

What does matter for this tutorial is the interfaces that are exposed. Once a WebSocket connection has been established to this server, it accepts plain text messages in the following formats, and issues JSON-formatted responses.

The requests it handles are:

(Note that the term "measurement" is used to describe a distinct data series within this system; in other systems, these have been called channels, mnemonics, telemetry points, or other names. No preference is made here; Open MCT is easily adapted to use the terminology appropriate to your system.) Additionally, while running the server from the terminal we can toggle the state of the "spacecraft" by hitting enter; this will turn the "thrusters" on and off, having observable changes in telemetry.

The telemetry dictionary referenced previously is contained in a separate file, used by the server. It uses a custom format and, for purposes of example, contains three "subsystems" containing a mix of numeric and string-based telemetry.

{
    "name": "Example Spacecraft",
    "identifier": "sc",
    "subsystems": [
        {
            "name": "Propulsion",
            "identifier": "prop",
            "measurements": [
                {
                    "name": "Fuel",
                    "identifier": "prop.fuel",
                    "units": "kilograms",
                    "type": "float"
                },
                {
                    "name": "Thrusters",
                    "identifier": "prop.thrusters",
                    "units": "None",
                    "type": "string"
                }
            ]
        },
        {
            "name": "Communications",
            "identifier": "comms",
            "measurements": [
                {
                    "name": "Received",
                    "identifier": "comms.recd",
                    "units": "bytes",
                    "type": "integer"
                },
                {
                    "name": "Sent",
                    "identifier": "comms.sent",
                    "units": "bytes",
                    "type": "integer"
                }
            ]
        },
        {
            "name": "Power",
            "identifier": "pwr",
            "measurements": [
                {
                    "name": "Generator Temperature",
                    "identifier": "pwr.temp",
                    "units": "\u0080C",
                    "type": "float"
                },
                {
                    "name": "Generator Current",
                    "identifier": "pwr.c",
                    "units": "A",
                    "type": "float"
                },
                {
                    "name": "Generator Voltage",
                    "identifier": "pwr.v",
                    "units": "V",
                    "type": "float"
                }
            ]
        }
    ]
}

tutorial-server/dictionary.json

It should be noted that neither the interface for the example server nor the dictionary format are expected by Open MCT; rather, these are intended to stand in for some existing source of telemetry data to which we wish to adapt Open MCT.

We can run this example server by:

cd tutorial-server
npm install ws
node app.js

To verify that this is running and try out its interface, we can use a tool like https://www.npmjs.com/package/wscat :

wscat -c ws://localhost:8081
connected (press CTRL+C to quit)
> dictionary
< {"type":"dictionary","value":{"name":"Example Spacecraft","identifier":"sc","subsystems":[{"name":"Propulsion","identifier":"prop","measurements":[{"name":"Fuel","identifier":"prop.fuel","units":"kilograms","type":"float"},{"name":"Thrusters","identifier":"prop.thrusters","units":"None","type":"string"}]},{"name":"Communications","identifier":"comms","measurements":[{"name":"Received","identifier":"comms.recd","units":"bytes","type":"integer"},{"name":"Sent","identifier":"comms.sent","units":"bytes","type":"integer"}]},{"name":"Power","identifier":"pwr","measurements":[{"name":"Generator Temperature","identifier":"pwr.temp","units":"€C","type":"float"},{"name":"Generator Current","identifier":"pwr.c","units":"A","type":"float"},{"name":"Generator Voltage","identifier":"pwr.v","units":"V","type":"float"}]}]}}

Now that the example server's interface is reasonably well-understood, a plugin can be written to adapt Open MCT to utilize it.

Step 1-Add a Top-level Object

Since Open MCT uses an "object-first" approach to accessing data, before we'll be able to do anything with this new data source, we'll need to have a way to explore the available measurements in the tree. In this step, we will add a top-level object which will serve as a container; in the next step, we will populate this with the contents of the telemetry dictionary (which we will retrieve from the server.)

define([
    'openmct'
], function (
    openmct
) {
    openmct.legacyRegistry.register("tutorials/telemetry", {
        "name": "Example Telemetry Adapter",
        "extensions": {
            "types": [
                {
                    "name": "Spacecraft",
                    "key": "example.spacecraft",
                    "cssClass": "icon-object"
                }
            ],
            "roots": [
                {
                    "id": "example:sc",
                    "priority": "preferred"
                }
            ],
            "models": [
                {
                    "id": "example:sc",
                    "model": {
                        "type": "example.spacecraft",
                        "name": "My Spacecraft",
                        "location": "ROOT",
                        "composition": []
                    }
                }
            ]
        }
    });
});

tutorials/telemetry/bundle.js

Here, we've created our initial telemetry plugin. This exposes a new domain object type (the "Spacecraft", which will be represented by the contents of the telemetry dictionary) and also adds one instance of it as a root-level object (by declaring an extension of category roots.) We have also set priority to preferred so that this shows up near the top, instead of below My Items.

If we include this in our set of active bundles:

<!--
 Open MCT, Copyright (c) 2014-2016, United States Government
 as represented by the Administrator of the National Aeronautics and Space
 Administration. All rights reserved.

 Open MCT is licensed under the Apache License, Version 2.0 (the
 "License"); you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0.

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 License for the specific language governing permissions and limitations
 under the License.

 Open MCT includes source code licensed under additional open source
 licenses. See the Open Source Licenses file (LICENSES.md) included with
 this source code distribution or the Licensing information page available
 at runtime from the About dialog for additional information.
-->
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title></title>
    <script src="bower_components/requirejs/require.js">
    </script>
    <script>
        require([
            'openmct',
            './tutorials/telemetry/bundle'
        ], function (openmct) {
            [
                'example/imagery',
                'example/eventGenerator',
                'example/generator',
                'platform/features/my-items',
                'platform/persistence/local',
                'tutorials/telemetry'
            ].forEach(
                openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
            );
            openmct.start();
        });
    </script>
    <link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
    <link rel="stylesheet" href="platform/commonUI/general/res/css/openmct.css">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-32x32.png" sizes="32x32">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-96x96.png" sizes="96x96">
    <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-16x16.png" sizes="16x16">
    <link rel="shortcut icon" href="platform/commonUI/general/res/images/favicons/favicon.ico">
</head>
<body class="user-environ">
    <div class="l-splash-holder s-splash-holder">
        <div class="l-splash s-splash"></div>
    </div>
</body>
</html>

index.html

...we will be able to reload Open MCT and see that it is present:

Telemetry

Now, we have somewhere in the UI to put the contents of our telemetry dictionary.

Step 2-Expose the Telemetry Dictionary

In order to expose the telemetry dictionary, we first need to read it from the server. Our first step will be to add a service that will handle interactions with the server; this will not be used by Open MCT directly, but will be used by subsequent components we add.

/*global define,WebSocket*/

define(
    [],
    function () {
        "use strict";

        function ExampleTelemetryServerAdapter($q, wsUrl) {
            var ws = new WebSocket(wsUrl),
                dictionary = $q.defer();

            // Handle an incoming message from the server
            ws.onmessage = function (event) {
                var message = JSON.parse(event.data);

                switch (message.type) {
                case "dictionary":
                    dictionary.resolve(message.value);
                    break;
                }
            };

            // Request dictionary once connection is established
            ws.onopen = function () {
                ws.send("dictionary");
            };

            return {
                dictionary: function () {
                    return dictionary.promise;
                }
            };
        }

        return ExampleTelemetryServerAdapter;
    }
);

tutorials/telemetry/src/ExampleTelemetryServerAdapter.js

When created, this service initiates a connection to the server, and begins loading the dictionary. This will occur asynchronously, so the dictionary() method it exposes returns a Promise for the loaded dictionary (dictionary.json from above), using Angular's $q (see https://docs.angularjs.org/api/ng/service/$q .) Note that error- and close-handling for this WebSocket connection have been omitted for brevity.

Once the dictionary has been loaded, we will want to represent its contents as domain objects. Specifically, we want subsystems to appear as objects under My Spacecraft, and measurements to appear as objects within those subsystems. This means that we need to convert the data from the dictionary into domain object models, and expose these to Open MCT via a modelService.

/*global define*/

define(
    function () {
        "use strict";

        var PREFIX = "example_tlm:",
            FORMAT_MAPPINGS = {
                float: "number",
                integer: "number",
                string: "string"
            };

        function ExampleTelemetryModelProvider(adapter, $q) {
            var modelPromise, empty = $q.when({});

            // Check if this model is in our dictionary (by prefix)
            function isRelevant(id) {
                return id.indexOf(PREFIX) === 0;
            }

            // Build a domain object identifier by adding a prefix
            function makeId(element) {
                return PREFIX + element.identifier;
            }

            // Create domain object models from this dictionary
            function buildTaxonomy(dictionary) {
                var models = {};

                // Create & store a domain object model for a measurement
                function addMeasurement(measurement) {
                    var format = FORMAT_MAPPINGS[measurement.type];
                    models[makeId(measurement)] = {
                        type: "example.measurement",
                        name: measurement.name,
                        telemetry: {
                            key: measurement.identifier,
                            ranges: [{
                                key: "value",
                                name: "Value",
                                units: measurement.units,
                                format: format
                            }]
                        }
                    };
                }

                // Create & store a domain object model for a subsystem
                function addSubsystem(subsystem) {
                    var measurements =
                        (subsystem.measurements || []);
                    models[makeId(subsystem)] = {
                        type: "example.subsystem",
                        name: subsystem.name,
                        composition: measurements.map(makeId)
                    };
                    measurements.forEach(addMeasurement);
                }

                (dictionary.subsystems || []).forEach(addSubsystem);

                return models;
            }

            // Begin generating models once the dictionary is available
            modelPromise = adapter.dictionary().then(buildTaxonomy);

            return {
                getModels: function (ids) {
                    // Return models for the dictionary only when they
                    // are relevant to the request.
                    return ids.some(isRelevant) ? modelPromise : empty;
                }
            };
        }

        return ExampleTelemetryModelProvider;
    }
);

tutorials/telemetry/src/ExampleTelemetryModelProvider.js

This script implements a provider for modelService; the modelService is a composite service, meaning that multiple such services can exist side by side. (For example, there is another provider for modelService that reads domain object models from the persistence store.)

Here, we read the dictionary using the server adapter from above; since this will be loaded asynchronously, we use promise-chaining (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#Chaining ) to take that result and build up an object mapping identifiers to new domain object models. This is returned from our modelService, but only when the request actually calls for identifiers that look like they're from the dictionary. This means that loading other models is not blocked by loading the dictionary. (Note that the modelService contract allows us to return either a sub- or superset of the requested models, so it is fine to always return the whole dictionary.)

Some notable points to call out here:

This allows our telemetry dictionary to be expressed as domain object models (and, in turn, as domain objects), but these objects still aren't reachable. To fix this, we will need another script which will add these subsystems to the root-level object we added in Step 1.

/*global define*/

define(
    function () {
        "use strict";

        var TAXONOMY_ID = "example:sc",
            PREFIX = "example_tlm:";

        function ExampleTelemetryInitializer(adapter, objectService) {
            // Generate a domain object identifier for a dictionary element
            function makeId(element) {
                return PREFIX + element.identifier;
            }

            // When the dictionary is available, add all subsystems
            // to the composition of My Spacecraft
            function initializeTaxonomy(dictionary) {
                // Get the top-level container for dictionary objects
                // from a group of domain objects.
                function getTaxonomyObject(domainObjects) {
                    return domainObjects[TAXONOMY_ID];
                }

                // Populate
                function populateModel(taxonomyObject) {
                    return taxonomyObject.useCapability(
                        "mutation",
                        function (model) {
                            model.name =
                                dictionary.name;
                            model.composition =
                                dictionary.subsystems.map(makeId);
                        }
                    );
                }

                // Look up My Spacecraft, and populate it accordingly.
                objectService.getObjects([TAXONOMY_ID])
                    .then(getTaxonomyObject)
                    .then(populateModel);
            }

            adapter.dictionary().then(initializeTaxonomy);
        }

        return ExampleTelemetryInitializer;
    }
);

tutorials/telemetry/src/ExampleTelemetryInitializer.js

At the conclusion of Step 1, the top-level My Spacecraft object was empty. This script will wait for the dictionary to be loaded, then load My Spacecraft (by its identifier), and "mutate" it. The mutation capability allows changes to be made to a domain object's model. Here, we take this top-level object, update its name to match what was in the dictionary, and set its composition to an array of domain object identifiers for all subsystems contained in the dictionary (using the same identifier prefix as before.)

Finally, we wire in these changes by modifying our plugin's bundle.js to provide metadata about how these pieces interact (both with each other, and with the platform):

define([
    'openmct',
+   './src/ExampleTelemetryServerAdapter',
+   './src/ExampleTelemetryInitializer',
+   './src/ExampleTelemetryModelProvider'
], function (
    openmct,
+   ExampleTelemetryServerAdapter,
+   ExampleTelemetryInitializer,
+   ExampleTelemetryModelProvider
) {
    openmct.legacyRegistry.register("tutorials/telemetry", {
    "name": "Example Telemetry Adapter",
    "extensions": {
        "types": [
            {
                "name": "Spacecraft",
                "key": "example.spacecraft",
                "cssClass": "icon-object"
            },
+           {
+               "name": "Subsystem",
+               "key": "example.subsystem",
+               "cssClass": "icon-object",
+               "model": { "composition": [] }
+           },
+           {
+               "name": "Measurement",
+               "key": "example.measurement",
+               "cssClass": "icon-telemetry",
+               "model": { "telemetry": {} },
+               "telemetry": {
+                   "source": "example.source",
+                   "domains": [
+                       {
+                           "name": "Time",
+                           "key": "timestamp"
+                       }
+                   ]
+               }
+           }
        ],
        "roots": [
            {
                "id": "example:sc",
                "priority": "preferred",
            }
        ],
        "models": [
            {
                "id": "example:sc",
                "model": {
                    "type": "example.spacecraft",
                    "name": "My Spacecraft",
                    "location": "ROOT",
                    "composition": []
                }
            }
        ],
+       "services": [
+           {
+               "key": "example.adapter",
+               "implementation": ExampleTelemetryServerAdapter,
+               "depends": [ "$q", "EXAMPLE_WS_URL" ]
+           }
+       ],
+       "constants": [
+           {
+               "key": "EXAMPLE_WS_URL",
+               "priority": "fallback",
+               "value": "ws://localhost:8081"
+           }
+       ],
+       "runs": [
+           {
+               "implementation": ExampleTelemetryInitializer,
+               "depends": [ "example.adapter", "objectService" ]
+           }
+       ],
+       "components": [
+           {
+               "provides": "modelService",
+               "type": "provider",
+               "implementation": ExampleTelemetryModelProvider,
+               "depends": [ "example.adapter", "$q" ]
+           }
+       ]
        }
    });
});

tutorials/telemetry/bundle.js

A summary of what we've added here:

Now if we run Open MCT (assuming our example telemetry server is also running) and expand our top-level node completely, we see the contents of our dictionary:

Telemetry 2

Note that "My Spacecraft" has changed its name to "Example Spacecraft", which is the name it had in the dictionary.

Step 3-Historical Telemetry

After Step 2, we are able to see our dictionary in the user interface and click around our different measurements, but we don't see any data. We need to give ourselves the ability to retrieve this data from the server. In this step, we will do so for the server's historical telemetry.

Our first step will be to add a method to our server adapter which allows us to send history requests to the server:

/*global define,WebSocket*/

define(
    [],
    function () {
        "use strict";

        function ExampleTelemetryServerAdapter($q, wsUrl) {
            var ws = new WebSocket(wsUrl),
+               histories = {},
                dictionary = $q.defer();

            // Handle an incoming message from the server
            ws.onmessage = function (event) {
                var message = JSON.parse(event.data);

                switch (message.type) {
                case "dictionary":
                    dictionary.resolve(message.value);
                    break;
+               case "history":
+                   histories[message.id].resolve(message);
+                   delete histories[message.id];
+                   break;
                }
            };

            // Request dictionary once connection is established
            ws.onopen = function () {
                ws.send("dictionary");
            };

            return {
                dictionary: function () {
                    return dictionary.promise;
                },
+               history: function (id) {
+                   histories[id] = histories[id] || $q.defer();
+                   ws.send("history " + id);
+                   return histories[id].promise;
+               }
            };
        }

        return ExampleTelemetryServerAdapter;
    }
);

tutorials/telemetry/src/ExampleTelemetryServerAdapter.js

When the history method is called, a new request is issued to the server for historical telemetry, unless a request for the same historical telemetry is still pending. Similarly, when historical telemetry arrives for a given identifier, the pending promise is resolved.

This history method will be used by a telemetryService provider which we will implement:

/*global define*/

define(
    ['./ExampleTelemetrySeries'],
    function (ExampleTelemetrySeries) {
        "use strict";

        var SOURCE = "example.source";

        function ExampleTelemetryProvider(adapter, $q) {
            // Used to filter out requests for telemetry
            // from some other source
            function matchesSource(request) {
                return (request.source === SOURCE);
            }

            return {
                requestTelemetry: function (requests) {
                    var packaged = {},
                        relevantReqs = requests.filter(matchesSource);

                    // Package historical telemetry that has been received
                    function addToPackage(history) {
                        packaged[SOURCE][history.id] =
                            new ExampleTelemetrySeries(history.value);
                    }

                    // Retrieve telemetry for a specific measurement
                    function handleRequest(request) {
                        var key = request.key;
                        return adapter.history(key).then(addToPackage);
                    }

                    packaged[SOURCE] = {};
                    return $q.all(relevantReqs.map(handleRequest))
                        .then(function () { return packaged; });
                },
                subscribe: function (callback, requests) {
                    return function () {};
                }
            };
        }

        return ExampleTelemetryProvider;
    }
);

tutorials/telemetry/src/ExampleTelemetryProvider.js

The requestTelemetry method of a telemetryService is expected to take an array of requests (each with source and key parameters, identifying the general source of data and the specific element within that source, respectively) and return a Promise for any telemetry data it knows of which satisfies those requests, packaged in a specific way. This packaging is as an object containing key-value pairs, where keys correspond to source properties of requests and values are key-value pairs, where keys correspond to key properties of requests and values are TelemetrySeries objects. (We will see our implementation below.)

To do this, we create a container for our telemetry source, and consult the adapter to get telemetry histories for any relevant requests, then package them as they come in. The $q.all method is used to return a single Promise that will resolve only when all histories have been packaged. Promise-chaining is used to ensure that the resolved value will be the fully-packaged data.

It is worth mentioning here that the requests we receive should look a little familiar. When Open MCT generates a request object associated with a domain object, it does so by merging together three JavaScript objects:

As such, the source and key properties we observe here will come from the type definition and domain object model, respectively, as we specified them during Step 2. (Or, they might come from somewhere else entirely, if we have other telemetry-providing domain objects in our system; that is something we check for using the source property.)

Finally, note that we also have a subscribe method, to satisfy the interface of telemetryService, but this subscribe method currently does nothing.

This script uses an ExampleTelemetrySeries class, which looks like:

/*global define*/

define(
    function () {
        "use strict";

        function ExampleTelemetrySeries(data) {
            return {
                getPointCount: function () {
                    return data.length;
                },
                getDomainValue: function (index) {
                    return (data[index] || {}).timestamp;
                },
                getRangeValue: function (index) {
                    return (data[index] || {}).value;
                }
            };
        }

        return ExampleTelemetrySeries;
    }
);

tutorials/telemetry/src/ExampleTelemetrySeries.js

This takes the array of telemetry values (as returned by the server) and wraps it with the interface expected by the platform (the methods shown.)

Finally, we expose this telemetryService provider declaratively:

define([
    'openmct',
    './src/ExampleTelemetryServerAdapter',
    './src/ExampleTelemetryInitializer',
    './src/ExampleTelemetryModelProvider'
], function (
    openmct,
    ExampleTelemetryServerAdapter,
    ExampleTelemetryInitializer,
    ExampleTelemetryModelProvider
) {
    openmct.legacyRegistry.register("tutorials/telemetry", {
    "name": "Example Telemetry Adapter",
    "extensions": {
        "types": [
            {
                "name": "Spacecraft",
                "key": "example.spacecraft",
                "cssClass": "icon-object"
            },
            {
                "name": "Subsystem",
                "key": "example.subsystem",
                "cssClass": "icon-object",
                "model": { "composition": [] }
            },
            {
                "name": "Measurement",
                "key": "example.measurement",
                "cssClass": "icon-telemetry",
                "model": { "telemetry": {} },
                "telemetry": {
                    "source": "example.source",
                    "domains": [
                        {
                            "name": "Time",
                            "key": "timestamp"
                        }
                    ]
                }
            }
        ],
        "roots": [
            {
                "id": "example:sc",
                "priority": "preferred"
            }
        ],
        "models": [
            {
                "id": "example:sc",
                "model": {
                    "type": "example.spacecraft",
                    "name": "My Spacecraft",
                    "location": "ROOT",
                    "composition": []
                }
            }
        ],
        "services": [
            {
                "key": "example.adapter",
                "implementation": "ExampleTelemetryServerAdapter.js",
                "depends": [ "$q", "EXAMPLE_WS_URL" ]
            }
        ],
        "constants": [
            {
                "key": "EXAMPLE_WS_URL",
                "priority": "fallback",
                "value": "ws://localhost:8081"
            }
        ],
        "runs": [
            {
                "implementation": "ExampleTelemetryInitializer.js",
                "depends": [ "example.adapter", "objectService" ]
            }
        ],
        "components": [
            {
                "provides": "modelService",
                "type": "provider",
                "implementation": "ExampleTelemetryModelProvider.js",
                "depends": [ "example.adapter", "$q" ]
            },
+           {
+               "provides": "telemetryService",
+               "type": "provider",
+               "implementation": "ExampleTelemetryProvider.js",
+               "depends": [ "example.adapter", "$q" ]
+           }
        ]
        }
    });
});

tutorials/telemetry/bundle.js

Now, if we navigate to one of our numeric measurements, we should see a plot of its historical telemetry:

Telemetry

We can now visualize our data, but it doesn't update over time - we know the server is continually producing new data, but we have to click away and come back to see it. We can fix this by adding support for telemetry subscriptions.

Step 4-Real-time Telemetry

Finally, we want to utilize the server's ability to subscribe to telemetry from Open MCT. To do this, first we want to expose some new methods for this from our server adapter:

/*global define,WebSocket*/

define(
    [],
    function () {
        "use strict";

        function ExampleTelemetryServerAdapter($q, wsUrl) {
            var ws = new WebSocket(wsUrl),
                histories = {},
+               listeners = [],
                dictionary = $q.defer();

            // Handle an incoming message from the server
            ws.onmessage = function (event) {
                var message = JSON.parse(event.data);

                switch (message.type) {
                case "dictionary":
                    dictionary.resolve(message.value);
                    break;
                case "history":
                    histories[message.id].resolve(message);
                    delete histories[message.id];
                    break;
+               case "data":
+                   listeners.forEach(function (listener) {
+                       listener(message);
+                   });
+                   break;
                }
            };

            // Request dictionary once connection is established
            ws.onopen = function () {
                ws.send("dictionary");
            };

            return {
                dictionary: function () {
                    return dictionary.promise;
                },
                history: function (id) {
                    histories[id] = histories[id] || $q.defer();
                    ws.send("history " + id);
                    return histories[id].promise;
                },
+               subscribe: function (id) {
+                   ws.send("subscribe " + id);
+               },
+               unsubscribe: function (id) {
+                   ws.send("unsubscribe " + id);
+               },
+               listen: function (callback) {
+                   listeners.push(callback);
+               }
            };
        }

        return ExampleTelemetryServerAdapter;
    }
);

tutorials/telemetry/src/ExampleTelemetryServerAdapter.js

Here, we have added subscribe and unsubscribe methods which issue the corresponding requests to the server. Separately, we introduce the ability to listen for data messages as they come in: These will contain the data associated with these subscriptions.

We then need only to utilize these methods from our telemetryService:

/*global define*/

define(
    ['./ExampleTelemetrySeries'],
    function (ExampleTelemetrySeries) {
        "use strict";

        var SOURCE = "example.source";

        function ExampleTelemetryProvider(adapter, $q) {
+           var subscribers = {};

            // Used to filter out requests for telemetry
            // from some other source
            function matchesSource(request) {
                return (request.source === SOURCE);
            }

+           // Listen for data, notify subscribers
+           adapter.listen(function (message) {
+               var packaged = {};
+               packaged[SOURCE] = {};
+               packaged[SOURCE][message.id] =
+                   new ExampleTelemetrySeries([message.value]);
+               (subscribers[message.id] || []).forEach(function (cb) {
+                   cb(packaged);
+               });
+           });

            return {
                requestTelemetry: function (requests) {
                    var packaged = {},
                        relevantReqs = requests.filter(matchesSource);

                    // Package historical telemetry that has been received
                    function addToPackage(history) {
                        packaged[SOURCE][history.id] =
                            new ExampleTelemetrySeries(history.value);
                    }

                    // Retrieve telemetry for a specific measurement
                    function handleRequest(request) {
                        var key = request.key;
                        return adapter.history(key).then(addToPackage);
                    }

                    packaged[SOURCE] = {};
                    return $q.all(relevantReqs.map(handleRequest))
                        .then(function () { return packaged; });
                },
                subscribe: function (callback, requests) {
+                   var keys = requests.filter(matchesSource)
+                       .map(function (req) { return req.key; });
+
+                   function notCallback(cb) {
+                       return cb !== callback;
+                   }
+
+                   function unsubscribe(key) {
+                       subscribers[key] =
+                           (subscribers[key] || []).filter(notCallback);
+                       if (subscribers[key].length < 1) {
+                           adapter.unsubscribe(key);
+                       }
+                   }
+
+                   keys.forEach(function (key) {
+                       subscribers[key] = subscribers[key] || [];
+                       adapter.subscribe(key);
+                       subscribers[key].push(callback);
+                   });
+
+                   return function () {
+                       keys.forEach(unsubscribe);
+                   };
                }
            };
        }

        return ExampleTelemetryProvider;
    }
);

tutorials/telemetry/src/ExampleTelemetryProvider.js

A quick summary of these changes:

Running Open MCT again, we can still plot our historical telemetry - but now we also see that it updates in real-time as more data comes in from the server.