diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md
index d93de3eae9..26c47fefbd 100644
--- a/docs/src/tutorials/index.md
+++ b/docs/src/tutorials/index.md
@@ -52,7 +52,7 @@ First step is to check out Open MCT Web from the source repository.
This will create a copy of the Open MCT Web source code repository in the folder
`openmctweb` (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 have a repository URL, use that as the "path to repo" above. Alternately,
if you received Open MCT Web 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 Web v0.6.2
@@ -66,10 +66,10 @@ At this point, it will also be useful to branch off of Open MCT Web v0.6.2
In its default configuration, Open MCT Web will try to use ElasticSearch
(expected to be deployed at /elastic on the same HTTP server running Open MCT
-Web) to persist user-created domain objects. We don’t need that for these
+Web) to persist user-created domain objects. We don't need that for these
tutorials, so we will replace the ElasticSearch plugin with the example
-persistence plugin. This doesn’t actually persist, so anything we create within
-Open MCT Web will be lost on reload, but that’s fine for purposes of these
+persistence plugin. This doesn't actually persist, so anything we create within
+Open MCT Web will be lost on reload, but that's fine for purposes of these
tutorials.
To change this configuration, edit bundles.json (at the top level of the Open
@@ -148,10 +148,10 @@ Once running, you should be able to view Open MCT Web from your browser at
http://localhost:8080/ (assuming the web server is running on port 8080,
and OpenMCTWeb is installed at the server's root path).
[Google Chrome](https://www.google.com/chrome/) is recommended for these
-tutorials, as Chrome is Open MCT Web’s “test-to” browser. The browser cache
+tutorials, as Chrome is Open MCT Web'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
+with Developer Tools expanded, and "Disable cache" selected from the Network
tab, as shown below.

@@ -160,9 +160,9 @@ tab, as shown below.
These tutorials cover three of the common tasks in Open MCT Web:
-* The “to-do list” tutorial illustrates how to add a new application feature.
-* The “bar graph” tutorial illustrates how to add a new telemetry visualization.
-* The “data set reader” tutorial illustrates how to integrate with a telemetry
+* The "to-do list" tutorial illustrates how to add a new application feature.
+* The "bar graph" tutorial illustrates how to add a new telemetry visualization.
+* The "data set reader" tutorial illustrates how to integrate with a telemetry
backend.
## To-do List
@@ -181,7 +181,7 @@ will be. The syntax of this file is described in more detail in the Open MCT Web
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”,
+to this plugin as tutorials/todo as well.) We will start with an "empty bundle",
one which exposes no extensions - which looks like:
```diff
@@ -198,6 +198,7 @@ __tutorials/todo/bundle.json__
We will also include this in our list of active bundles.
#### Before
+```diff
[
"platform/framework",
"platform/core",
@@ -219,7 +220,8 @@ We will also include this in our list of active bundles.
"example/persistence",
"example/generator"
- ]
+]
+```
__bundles.json__
#### After
@@ -252,8 +254,8 @@ __bundles.json__
```
__bundles.json__
-At this point, we can reload Open MCT Web. We haven’t introduced any new
-functionality, so we don’t see anything different, but if we run with logging
+At this point, we can reload Open MCT Web. 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:
@@ -269,7 +271,7 @@ the work that the Open MCT Web 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 Web Developer Guide.)
-In the case of our to-do list feature, the to-do list itself is the thing we’ll
+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:
```diff
@@ -291,60 +293,63 @@ our bundle definition:
```
__tutorials/todo/bundle.json__
-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
+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:
+Going through the properties we've defined:
* The `key` of `example.todo` will be stored as the machine-readable name for
domain objects of this type.
-* The `name` of “To-Do List” is the human-readable name for this type, and will
+* The `name` of "To-Do List" is the human-readable name for this type, and will
be shown to users.
-* The `glyph` refers to a special character in Open MCT Web’s custom font set;
+* The `glyph` refers to a special character in Open MCT Web's custom font set;
this will be used as an icon.
* The `description` is also human-readable, and will be used whenever a longer
explanation of what this type is should be shown.
* Finally, the `features` property describes some special features of objects of
this type. Including `creation` here means that we want users to be able to
create this (in other cases, we may wish to expose things as domain objects
-which aren’t user-created, in which case we would omit this.)
+which aren't user-created, in which case we would omit this.)
If we reload Open MCT Web, we see that our new domain object type appears in the
Create menu:

-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.
+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 Web, the pattern that the user expects is that they’ll
+contents. In Open MCT Web, 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 Web, these visualizations are called views.
-A view in Open MCT Web is defined by an Angular template. We’ll add that in the
+A view in Open MCT Web 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.)
-
-
-
-
+```diff
+
+```
__tutorials/todo/res/templates/todo.html__
Finally, the `TodoController` uses the `dialogService` now, so we need to
declare that dependency in its extension definition:
-
- {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "glyph": "j",
- "description": "A list of things that need to be done.",
- "features": ["creation"],
- "model": {
- "tasks": [
- { "description": "Add a type", "completed": true },
- { "description": "Add a view" }
- ]
- }
+```diff
+{
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "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",
- "glyph": "j",
- "name": "List",
- "templateUrl": "templates/todo.html",
- "toolbar": {
- "sections": [
- {
- "items": [
- {
- "text": "Add Task",
- "glyph": "+",
- "method": "addTask",
- "control": "button"
- }
- ]
- },
- {
- "items": [
- {
- "glyph": "Z",
- "method": "removeTask",
- "control": "button"
- }
- ]
- }
- ]
- }
+ }
+ ],
+ "views": [
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "glyph": "j",
+ "name": "List",
+ "templateUrl": "templates/todo.html",
+ "toolbar": {
+ "sections": [
+ {
+ "items": [
+ {
+ "text": "Add Task",
+ "glyph": "+",
+ "method": "addTask",
+ "control": "button"
+ }
+ ]
+ },
+ {
+ "items": [
+ {
+ "glyph": "Z",
+ "method": "removeTask",
+ "control": "button"
+ }
+ ]
+ }
+ ]
}
- ],
- "controllers": [
- {
- "key": "TodoController",
- "implementation": "controllers/TodoController.js",
- + "depends": [ "$scope", "dialogService" ]
- }
- ]
- }
+ }
+ ],
+ "controllers": [
+ {
+ "key": "TodoController",
+ "implementation": "controllers/TodoController.js",
++ "depends": [ "$scope", "dialogService" ]
+ }
+ ]
}
+}
+```
__tutorials/todo/bundle.json__
-If we now reload Open MCT Web, we’ll be able to see the new functionality we’ve
+If we now reload Open MCT Web, 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:
+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:

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

-Finally, if we click on the description of a specific task, we’ll see a new
+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:

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.
+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
@@ -955,107 +975,108 @@ In this section, our goal is to:
* Tweak the general aesthetics to our liking.
* Get rid of those default tasks (we can create our own now.)
-To support the first two, we’ll need to expose some methods for checking these
+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
- }]
+```diff
+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);
- }
- });
- }
+ }]
+ };
+
+ 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();
}
-
- return TodoController;
- });
+
+ // 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:
@@ -1074,35 +1095,38 @@ 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;
- }
+```diff
+.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:
@@ -1115,104 +1139,106 @@ Here, we have defined classes and appearances for:
To include this CSS file in our running instance of Open MCT Web, we need to
declare it in our bundle definition, this time as an extension of category
`stylesheets`:
-
- {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "glyph": "j",
- "description": "A list of things that need to be done.",
- "features": ["creation"],
- "model": {
- "tasks": []
- }
+```diff
+{
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"],
+ "model": {
+ "tasks": []
}
- ],
- "views": [
- {
- "key": "example.todo",
- "type": "example.todo",
- "glyph": "j",
- "name": "List",
- "templateUrl": "templates/todo.html",
- "toolbar": {
- "sections": [
- {
- "items": [
- {
- "text": "Add Task",
- "glyph": "+",
- "method": "addTask",
- "control": "button"
- }
- ]
- },
- {
- "items": [
- {
- "glyph": "Z",
- "method": "removeTask",
- "control": "button"
- }
- ]
- }
- ]
- }
+ }
+ ],
+ "views": [
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "glyph": "j",
+ "name": "List",
+ "templateUrl": "templates/todo.html",
+ "toolbar": {
+ "sections": [
+ {
+ "items": [
+ {
+ "text": "Add Task",
+ "glyph": "+",
+ "method": "addTask",
+ "control": "button"
+ }
+ ]
+ },
+ {
+ "items": [
+ {
+ "glyph": "Z",
+ "method": "removeTask",
+ "control": "button"
+ }
+ ]
+ }
+ ]
}
- ],
- "controllers": [
- {
- "key": "TodoController",
- "implementation": "controllers/TodoController.js",
- "depends": [ "$scope", "dialogService" ]
- }
- ],
- + "stylesheets": [
- + {
- + "stylesheetUrl": "css/todo.css"
- + }
- + ]
- }
+ }
+ ],
+ "controllers": [
+ {
+ "key": "TodoController",
+ "implementation": "controllers/TodoController.js",
+ "depends": [ "$scope", "dialogService" ]
+ }
+ ],
++ "stylesheets": [
++ {
++ "stylesheetUrl": "css/todo.css"
++ }
++ ]
}
+}
+```
__tutorials/todo/bundle.json__
-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.
+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:
+Finally, let's utilize these changes from our view's template:
+```diff
++
++
- +
- +
-
-
- +
- + There are no tasks to show.
- +
- +
+
++
++ There are no tasks to show.
++
++
+```
__tutorials/todo/res/templates/todo.html__
Now, if we reload our page and create a new To-Do List, we will initially see:
@@ -1239,31 +1265,34 @@ there will be addressed in more brevity here.
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
+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:
- {
- "name": "Bar Graph",
- "description": "Provides the Bar Graph view of telemetry elements.",
- "extensions": {
- "views": [
- {
- "name": "Bar Graph",
- "key": "example.bargraph",
- "glyph": "H",
- "templateUrl": "templates/bargraph.html",
- "needs": [ "telemetry" ],
- "delegation": true
- }
- ],
- "stylesheets": [
- {
- "stylesheetUrl": "css/bargraph.css"
- }
- ]
- }
+```diff
+{
+ "name": "Bar Graph",
+ "description": "Provides the Bar Graph view of telemetry elements.",
+ "extensions": {
+ "views": [
+ {
+ "name": "Bar Graph",
+ "key": "example.bargraph",
+ "glyph": "H",
+ "templateUrl": "templates/bargraph.html",
+ "needs": [ "telemetry" ],
+ "delegation": true
+ }
+ ],
+ "stylesheets": [
+ {
+ "stylesheetUrl": "css/bargraph.css"
+ }
+ ]
}
+}
+```
+
__tutorials/bargraph/bundle.json__
The view definition should look familiar after the To-Do List tutorial, with
@@ -1278,49 +1307,51 @@ via capability delegation; that is, by domain objects which delegate the
used for Telemetry Panel objects as well as for individual telemetry-providing
domain objects.
-For this tutorial, we’ll assume that we’ve sketched out our template and CSS
+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:
-
-
-
-
-
-
-
-
+```diff
+
+
+
+
+
-
-
-
- Label A
+
+
-
- Label B
-
-
- Label C
+
+
+
+
+
+
+
+ Label A
+
+
+ Label B
+
+
+ Label C
+
+
+
+```
__tutorials/bargraph/res/templates/bargraph.html__
Here, three regions are defined. The first will be for tick labels along the
@@ -1330,80 +1361,81 @@ 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:
+```diff
+.example-bargraph {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ mid-width: 160px;
+ min-height: 160px;
+}
- .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;
- }
+.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 `bundles.json`,
+This is already enough that, if we add `"tutorials/bargraph"` to `bundles.json`,
we should be able to run Open MCT Web 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:
@@ -1415,10 +1447,10 @@ 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.
+Our next step will be to begin dynamically populating this template's contents.
Specifically, our goals for this step will be to:
-* Show one bar per telemetry-providing domain object (for which we’ll be getting
+* Show one bar per telemetry-providing domain object (for which we'll be getting
actual telemetry data in subsequent steps.)
* Show correct labels for these objects at the bottom.
* Show numeric labels on the left-hand side.
@@ -1426,40 +1458,41 @@ actual telemetry data in subsequent steps.)
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:
+```diff
+define(function () {
+ function BarGraphController($scope, telemetryHandler) {
+ var handle;
- 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;
- });
+ // 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:
+A summary of what we've done here:
-* We’re exposing some numeric values that will correspond to the _low_, _middle_,
+* We're exposing some numeric values that will correspond to the _low_, _middle_,
and _high_ end of the graph. (The `medium` attribute will be useful for
positioning the middle line, which are graphs will ultimately descend down or
push up from.)
@@ -1476,39 +1509,40 @@ 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:
-
-
-
-
-
-
-
+```diff
+
+```
__tutorials/bargraph/res/templates/bargraph.html__
Summarizing these changes:
@@ -1528,34 +1562,36 @@ 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.
- {
- "name": "Bar Graph",
- "description": "Provides the Bar Graph view of telemetry elements.",
- "extensions": {
- "views": [
- {
- "name": "Bar Graph",
- "key": "example.bargraph",
- "glyph": "H",
- "templateUrl": "templates/bargraph.html",
- "needs": [ "telemetry" ],
- "delegation": true
- }
- ],
- "stylesheets": [
- {
- "stylesheetUrl": "css/bargraph.css"
- }
- ],
- + "controllers": [
- + {
- + "key": "BarGraphController",
- + "implementation": "controllers/BarGraphController.js",
- + "depends": [ "$scope", "telemetryHandler" ]
- + }
- + ]
- }
+```diff
+{
+ "name": "Bar Graph",
+ "description": "Provides the Bar Graph view of telemetry elements.",
+ "extensions": {
+ "views": [
+ {
+ "name": "Bar Graph",
+ "key": "example.bargraph",
+ "glyph": "H",
+ "templateUrl": "templates/bargraph.html",
+ "needs": [ "telemetry" ],
+ "delegation": true
+ }
+ ],
+ "stylesheets": [
+ {
+ "stylesheetUrl": "css/bargraph.css"
+ }
+ ],
++ "controllers": [
++ {
++ "key": "BarGraphController",
++ "implementation": "controllers/BarGraphController.js",
++ "depends": [ "$scope", "telemetryHandler" ]
++ }
++ ]
}
+}
+```
__tutorials/bargraph/bundle.json__
When we reload Open MCT Web, we are now able to see that our bar graph view
@@ -1566,52 +1602,53 @@ this Telemetry Panel containing four Sine Wave Generators.
### Step 3-Using Telemetry Data
-Now that our bar graph is labeled correctly, it’s time to start putting 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
+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.
+```diff
+define(function () {
+ function BarGraphController($scope, telemetryHandler) {
+ var handle;
- 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;
- });
+ // 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
@@ -1624,41 +1661,44 @@ decide this.
Next, we utilize this functionality from the template:
-
-
-
-
-
-
-
-
-
-
+```diff
+
+'''
+
__tutorials/bargraph/res/templates/bargraph.html__
Here, we utilize the functions we just provided from the controller to position
@@ -1670,156 +1710,160 @@ When we reload Open MCT Web, our bar graph view now looks like:
### Step 4-View Configuration
-The default minimum and maximum values we’ve provided happen to make sense for
+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 Web platform exposes a configuration object - called
-`configuration` - into our view’s scope. We can populate it as we please, and
+`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:
+First, let's add a tool bar for changing these three values in Edit mode:
- {
- "name": "Bar Graph",
- "description": "Provides the Bar Graph view of telemetry elements.",
- "extensions": {
- "views": [
- {
- "name": "Bar Graph",
- "key": "example.bargraph",
- "glyph": "H",
- "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
- + }
- + ]
- + }
- ]
- }
+```diff
+{
+ "name": "Bar Graph",
+ "description": "Provides the Bar Graph view of telemetry elements.",
+ "extensions": {
+ "views": [
+ {
+ "name": "Bar Graph",
+ "key": "example.bargraph",
+ "glyph": "H",
+ "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": "controllers/BarGraphController.js",
- "depends": [ "$scope", "telemetryHandler" ]
- }
- ]
- }
+ }
+ ],
+ "stylesheets": [
+ {
+ "stylesheetUrl": "css/bargraph.css"
+ }
+ ],
+ "controllers": [
+ {
+ "key": "BarGraphController",
+ "implementation": "controllers/BarGraphController.js",
+ "depends": [ "$scope", "telemetryHandler" ]
+ }
+ ]
}
+}
+```
__tutorials/bargraph/bundle.json__
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`
+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);
+```diff
+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];
++ };
}
-
- return BarGraphController;
- });
+
++ // 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:
@@ -1829,11 +1873,11 @@ initializing them to explicit values. This is placed into its own function,
since it will be called a lot.
* The function `setDefault` is included; it will be used to set the default
values for `low`, `middle`, and `high` in the view configuration, but only if
-they aren’t present.
+they aren't present.
* The tool bar will treat properties in a view proxy as getter-setters if
they are functions; that is, they will be called with an argument to be used
as a setter, and with no argument to use as a getter. We provide ourselves a
-function for making these getter-setters (since we’ll need three) that
+function for making these getter-setters (since we'll need three) that
additionally handles some checking to ensure that these are actually numbers.
* After that, we actually initialize both the view `configuration` object with
defaults (if needed), and expose its state into the scope.
@@ -1866,134 +1910,135 @@ 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.
+```diff
+/*global require,process,console*/
- /*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));
+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]
+ }));
}
});
-
- // 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"]);
+
+ // 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
@@ -2011,108 +2056,110 @@ messages in the following formats, and issues JSON-formatted responses.
The requests it handles are:
* `dictionary`: Responds with a JSON response with the following fields:
- * `type`: “dictionary”
+ * `type`: "dictionary"
* `value`: … the telemetry dictionary (see below) …
* `subscribe
`: Subscribe to new telemetry data for the measurement with
the provided identifier. The server will begin sending messages of the
following form:
- * `type`: “data”
+ * `type`: "data"
* `id`: The identifier for the measurement.
* `value`: An object containing the actual measurement, in two fields:
- * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement”
+ * `timestamp`: A UNIX timestamp (in milliseconds) for the "measurement"
* `value`: The data value for the measurement (either a number, or a
string)
* `unsubscribe `: Stop receiving new data for the identified measurement.
* `history `: Request a history of all telemetry data for the identified
measurement.
- * `type`: “history”
+ * `type`: "history"
* `id`: The identifier for the measurement.
* `value`: An array of objects containing the actual measurement, each of
which having two fields:
- * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement”
+ * `timestamp`: A UNIX timestamp (in milliseconds) for the "measurement"
* `value`: The data value for the measurement (either a number, or
a string)
-(Note that the term “measurement” is used to describe a distinct data series
+(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 Web 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”
+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
+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"
- }
- ]
- }
- ]
- }
+```diff
+{
+ "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
@@ -2134,13 +2181,13 @@ like https://www.npmjs.com/package/wscat :
> 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
+Now that the example server's interface is reasonably well-understood, a plugin
can be written to adapt Open MCT Web to utilize it.
### Step 1-Add a Top-level Object
-Since Open MCT Web 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
+Since Open MCT Web 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
@@ -2171,60 +2218,63 @@ will retrieve from the server.)
}
__tutorials/telemetry/bundle.json__
-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
+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:
- [
- "platform/framework",
- "platform/core",
- "platform/representation",
- "platform/commonUI/about",
- "platform/commonUI/browse",
- "platform/commonUI/edit",
- "platform/commonUI/dialog",
- "platform/commonUI/general",
- "platform/containment",
- "platform/telemetry",
- "platform/features/layout",
- "platform/features/pages",
- "platform/features/plot",
- "platform/features/scrolling",
- "platform/forms",
- "platform/persistence/queue",
- "platform/policy",
-
- "example/persistence",
- "example/generator"
- ]
- [
- "platform/framework",
- "platform/core",
- "platform/representation",
- "platform/commonUI/about",
- "platform/commonUI/browse",
- "platform/commonUI/edit",
- "platform/commonUI/dialog",
- "platform/commonUI/general",
- "platform/containment",
- "platform/telemetry",
- "platform/features/layout",
- "platform/features/pages",
- "platform/features/plot",
- "platform/features/scrolling",
- "platform/forms",
- "platform/persistence/queue",
- "platform/policy",
-
- "example/persistence",
- "example/generator",
-
- + "tutorials/telemetry"
- ]
+```diff
+[
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ "platform/policy",
+
+ "example/persistence",
+ "example/generator"
+]
+[
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ "platform/policy",
+
+ "example/persistence",
+ "example/generator",
+
++ "tutorials/telemetry"
+]
+```
+
__bundles.json__
...we will be able to reload Open MCT Web and see that it is present:
@@ -2283,7 +2333,7 @@ __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`
+(`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.
@@ -2386,7 +2436,7 @@ 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
+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
@@ -2418,11 +2468,11 @@ ordering properties of the data, but this will be the same for all
measurements, so we will define that later at the type level.)
* This field (whose contents will be merged atop the telemetry property we
define at the type-level) will serve as a template for later `telemetry`
-requests to the `telemetryService`, so we’ll see the properties we define here
+requests to the `telemetryService`, so we'll see the properties we define here
again later in Steps 3 and 4.
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
+(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.
@@ -2479,91 +2529,93 @@ __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
+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.json` to
+Finally, we wire in these changes by modifying our plugin's `bundle.json` to
provide metadata about how these pieces interact (both with each other, and
with the platform):
- {
- "name": "Example Telemetry Adapter",
- "extensions": {
- "types": [
- {
- "name": "Spacecraft",
- "key": "example.spacecraft",
- "glyph": "o"
- },
- {
- + "name": "Subsystem",
- + "key": "example.subsystem",
- + "glyph": "o",
- + "model": { "composition": [] }
- + },
- + {
- + "name": "Measurement",
- + "key": "example.measurement",
- + "glyph": "T",
- + "model": { "telemetry": {} },
- + "telemetry": {
- + "source": "example.source",
- + "domains": [
- + {
- + "name": "Time",
- + "key": "timestamp"
- + }
- + ]
- + }
- + }
- ],
- "roots": [
- {
- "id": "example:sc",
- "priority": "preferred",
- "model": {
- "type": "example.spacecraft",
- "name": "My Spacecraft",
- "composition": []
- }
+```diff
+{
+ "name": "Example Telemetry Adapter",
+ "extensions": {
+ "types": [
+ {
+ "name": "Spacecraft",
+ "key": "example.spacecraft",
+ "glyph": "o"
+ },
+ {
++ "name": "Subsystem",
++ "key": "example.subsystem",
++ "glyph": "o",
++ "model": { "composition": [] }
++ },
++ {
++ "name": "Measurement",
++ "key": "example.measurement",
++ "glyph": "T",
++ "model": { "telemetry": {} },
++ "telemetry": {
++ "source": "example.source",
++ "domains": [
++ {
++ "name": "Time",
++ "key": "timestamp"
++ }
++ ]
++ }
++ }
+ ],
+ "roots": [
+ {
+ "id": "example:sc",
+ "priority": "preferred",
+ "model": {
+ "type": "example.spacecraft",
+ "name": "My Spacecraft",
+ "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" ]
- + }
- + ]
- }
+ }
+ ],
++ "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" ]
++ }
++ ]
}
+}
+```
__tutorials/telemetry/bundle.json__
-A summary of what we’ve added here:
+A summary of what we've added here:
* New type definitions have been added to represent Subsystems and Measurements,
respectively.
@@ -2571,7 +2623,7 @@ respectively.
field added in the model, but contains properties that will be common among
all Measurements. In particular, the `source` field will be used later as a
symbolic identifier for the telemetry data source.
- * We have also added some “initial models” for these two types using the
+ * We have also added some "initial models" for these two types using the
`model` field. While domain objects of these types cannot be created via the
Create menu, some policies will look at initial models to predict what
capabilities domain objects of certain types would have, so we want to
@@ -2588,13 +2640,13 @@ different URLs for the WebSocket connection.
to ensure that this executes (and populates the contents of the top-level My
Spacecraft object) once Open MCT Web is started.
* This depends upon the `example.adapter` service we exposed above, as well
- as Angular’s `$q`; these services will be made available in the constructor
+ as Angular's `$q`; these services will be made available in the constructor
call.
* Finally, the `modelService` provider which presents dictionary elements as
domain object models is exposed. Since `modelService` is a composite service,
this is registered under the extension category `components`.
* As with the initializer, this depends upon the `example.adapter` service
- we exposed above, as well as Angular’s `$q`; these services will be made
+ we exposed above, as well as Angular's `$q`; these services will be made
available in the constructor call.
Now if we run Open MCT Web (assuming our example telemetry server is also
@@ -2604,67 +2656,68 @@ dictionary:

-Note that “My Spacecraft” has changed its name to “Example Spacecraft”, which
+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
+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.
+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;
- }
- );
+```diff
+/*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
@@ -2674,53 +2727,54 @@ identifier, the pending promise is resolved.
This `history` method will be used by a `telemetryService` provider which we
will implement:
+```diff
+/*global define*/
- /*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 () {};
- }
- };
+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 ExampleTelemetryProvider;
+
+ 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
@@ -2743,9 +2797,9 @@ It is worth mentioning here that the `requests` we receive should look a little
familiar. When Open MCT Web generates a `request` object associated with a
domain object, it does so by merging together three JavaScript objects:
-* First, the `telemetry` property from that domain object’s type definition.
-* Second, the `telemetry` property from that domain object’s model.
-* Finally, the `request` object that was passed in via that domain object’s
+* First, the `telemetry` property from that domain object's type definition.
+* Second, the `telemetry` property from that domain object's model.
+* Finally, the `request` object that was passed in via that domain object's
`telemetry` capability.
As such, the `source` and `key` properties we observe here will come from the
@@ -2758,115 +2812,117 @@ Finally, note that we also have a `subscribe` method, to satisfy the interface o
`telemetryService`, but this `subscribe` method currently does nothing.
This script uses an `ExampleTelemetrySeries` class, which looks like:
+```diff
+/*global define*/
- /*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;
+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:
-
- {
- "name": "Example Telemetry Adapter",
- "extensions": {
- "types": [
- {
- "name": "Spacecraft",
- "key": "example.spacecraft",
- "glyph": "o"
- },
- {
- "name": "Subsystem",
- "key": "example.subsystem",
- "glyph": "o",
- "model": { "composition": [] }
- },
- {
- "name": "Measurement",
- "key": "example.measurement",
- "glyph": "T",
- "model": { "telemetry": {} },
- "telemetry": {
- "source": "example.source",
- "domains": [
- {
- "name": "Time",
- "key": "timestamp"
- }
- ]
- }
+```diff
+{
+ "name": "Example Telemetry Adapter",
+ "extensions": {
+ "types": [
+ {
+ "name": "Spacecraft",
+ "key": "example.spacecraft",
+ "glyph": "o"
+ },
+ {
+ "name": "Subsystem",
+ "key": "example.subsystem",
+ "glyph": "o",
+ "model": { "composition": [] }
+ },
+ {
+ "name": "Measurement",
+ "key": "example.measurement",
+ "glyph": "T",
+ "model": { "telemetry": {} },
+ "telemetry": {
+ "source": "example.source",
+ "domains": [
+ {
+ "name": "Time",
+ "key": "timestamp"
+ }
+ ]
}
- ],
- "roots": [
- {
- "id": "example:sc",
- "priority": "preferred",
- "model": {
- "type": "example.spacecraft",
- "name": "My Spacecraft",
- "composition": []
- }
+ }
+ ],
+ "roots": [
+ {
+ "id": "example:sc",
+ "priority": "preferred",
+ "model": {
+ "type": "example.spacecraft",
+ "name": "My Spacecraft",
+ "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" ]
- + }
- ]
- }
+ }
+ ],
+ "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.json__
Now, if we navigate to one of our numeric measurements, we should see a plot of
@@ -2874,78 +2930,80 @@ its historical telemetry:

-We can now visualize our data, but it doesn’t update over time - we know the
+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
+Finally, we want to utilize the server's ability to subscribe to telemetry
from Open MCT Web. 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;
+```diff
+/*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
@@ -2955,88 +3013,91 @@ 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);
- + };
- }
- };
+```diff
+/*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);
}
-
- return ExampleTelemetryProvider;
+
++ // 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:
@@ -3052,7 +3113,7 @@ providing single-element series objects.)
subscribers. This method is expected to return a function which terminates the
subscription when called, so we do some work to remove subscribers in this
situations. When our subscriber count for a given measurement drops to zero,
-we issue an unsubscribe request. (We don’t take any care to avoid issuing
+we issue an unsubscribe request. (We don't take any care to avoid issuing
multiple subscribe requests to the server, because we happen to know that the
server can handle this.)