├── .gitignore ├── LICENSE.md ├── README.md ├── dictionary-plugin.js ├── dictionary.json ├── example-server ├── history-server.js ├── realtime-server.js ├── server.js ├── spacecraft.js └── static-server.js ├── historical-telemetry-plugin.js ├── images ├── object-tree.png ├── openmct-empty.png ├── openmct-missing-root.png ├── openmct-root-folder.png └── telemetry-objects.png ├── index.html ├── lib └── http.js ├── package-lock.json ├── package.json └── realtime-telemetry-plugin.js /.gitignore: -------------------------------------------------------------------------------- 1 | openmct 2 | node_modules 3 | npm-debug.log 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Open MCT License 2 | 3 | Open MCT, Copyright (c) 2014-2017, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved. 4 | 5 | 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. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open MCT Integration Tutorials 2 | 3 | These tutorials will walk you through the simple process of integrating your telemetry systems with Open MCT. In case you don't have any telemetry systems, we've included a reference implementation of a historical and realtime server. We'll take you through the process of integrating those services with Open MCT. 4 | 5 | ## Tutorial Prerequisites 6 | 7 | * [node.js](https://nodejs.org/en/) 8 | * Mac OS: We recommend using [Homebrew](https://brew.sh/) to install node. 9 | ``` 10 | $ brew install node 11 | ``` 12 | * Windows: https://nodejs.org/en/download/ 13 | * linux: https://nodejs.org/en/download/ 14 | * [git](https://git-scm.com/) 15 | * Mac OS: If XCode is installed, git is likely to already be available from your command line. If not, git can be installed using [Homebrew](https://brew.sh/). 16 | ``` 17 | $ brew install git 18 | ``` 19 | * Windows: https://git-scm.com/downloads 20 | * linux: https://git-scm.com/downloads 21 | 22 | Neither git nor node.js are requirements for using Open MCT, however this tutorial assumes that both are installed. Also, command line familiarity is a plus, however the tutorial is written in such a way that it should be possible to copy-paste the steps verbatim into a POSIX command line. 23 | The version of node to use can be found by inspecting the `engines` section of Open MCT here: https://www.npmjs.com/package/openmct?activeTab=code 24 | ## Installing the tutorials 25 | 26 | ``` 27 | git clone https://github.com/nasa/openmct-tutorial.git 28 | cd openmct-tutorial 29 | npm install 30 | npm start 31 | ``` 32 | 33 | This will clone the tutorials and install Open MCT from NPM. It will also install the dependencies needed to run the provided telemetry server. The last command will start the server. The telemetry server provided is for demonstration purposes only, and is not intended to be used in a production environment. 34 | 35 | At this point, you will be able to browse the tutorials in their completed state. We have also tagged the repository at each step of the tutorial, so it is possible to skip to a particular step using git checkout. 36 | 37 | eg. 38 | 39 | ``` 40 | git checkout -f part-X-step-N 41 | ``` 42 | 43 | Substituting the appropriate part and step numbers as necessary. 44 | 45 | The recommended way of following the tutorials is to checkout the first step (the command is shown below), and then follow the tutorial by manually adding the code, but if you do get stuck you can use the tags to skip ahead. If you do get stuck, please let us know by [filing in issue in this repository](https://github.com/nasa/openmct-tutorial/issues/new) so that we can improve the tutorials. 46 | 47 | ## Part A: Running Open MCT 48 | **Shortcut**: `git checkout -f part-a` 49 | 50 | We're going to define a single `index.html` page. We'll include the Open MCT library, configure a number of plugins, and then start the application. 51 | 52 | [index.html](https://github.com/nasa/openmct-tutorial/tree/part-b-step-1/index.html) 53 | ```html 54 | 55 | 56 | 57 | Open MCT Tutorials 58 | 59 | 60 | 74 | 75 | 76 |
77 | 78 | 79 | ``` 80 | 81 | We have provided a basic server for the purpose of this tutorial, which will act as a web server as well as a telemetry source. This server is for demonstration purposes only. The Open MCT web client can be hosted on any http server. 82 | 83 | If the server is not already running, run it now - 84 | 85 | ``` 86 | npm start 87 | ``` 88 | 89 | If you open a web browser and navigate to http://localhost:8080/ you will see the Open MCT application running. Currently it is populated with one object named `My Items`. 90 | 91 | ![Open MCT](images/openmct-empty.png) 92 | 93 | In this tutorial we will populate this tree with a number of objects representing telemetry points on a fictional spacecraft, and integrate with a telemetry server in order to receive and display telemetry for the spacecraft. 94 | 95 | # Part B - Populating the Object Tree 96 | ## Introduction 97 | In Open MCT everything is represented as a Domain Object, this includes sources of telemetry, telemetry points, and views for visualizing telemetry. Domain Objects are accessible from the object tree 98 | 99 | ![Domain Objects are accessible from the object tree](images/object-tree.png) 100 | 101 | The object tree is a hierarchical representation of all of the objects available in Open MCT. At the root of an object hierarchy is a root object. For this tutorial, we are going to create a new root object representing our spacecraft, and then populate it with objects representing the telemetry producing subsystems on our fictional spacecraft. 102 | 103 | ## Step 1 - Defining a new plugin 104 | **Shortcut:** `git checkout -f part-b-step-1` 105 | 106 | Let's start by creating a new plugin to populate the object tree. We will include all of the code for this plugin in a new javascript file named `dictionary-plugin.js`. Let's first create a very basic plugin that simply logs a message indicating that it's been installed. 107 | 108 | [dictionary-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-b-step-2/dictionary-plugin.js) 109 | ```javascript 110 | function DictionaryPlugin() { 111 | return function install() { 112 | console.log("I've been installed!"); 113 | } 114 | }; 115 | ``` 116 | 117 | Next, we'll update index.html to include the file and install the plugin: 118 | 119 | [index.html](https://github.com/nasa/openmct-tutorial/blob/part-b-step-2/index.html) 120 | ```html 121 | 122 | 123 | 124 | Open MCT Tutorials 125 | 126 | 127 | 128 | 144 | 145 | 146 |
147 | 148 | 149 | ``` 150 | 151 | If we reload the browser now, and open a javascript console, we should see the following message 152 | 153 | ``` 154 | I've been installed. 155 | ``` 156 | 157 | The process of opening a javascript console differs depending on the browser being used. Instructions for launching the browser console in most modern browsers are [available here](http://webmasters.stackexchange.com/a/77337). 158 | 159 | In summary, an Open MCT plugin is very simple: it's an initialization function which receives the Open MCT API as the single argument. It then uses the provided API to extend Open MCT. Generally, we like plugins to return an initialization function so they can receive configuration. 160 | 161 | [Learn more about plugins here](https://github.com/nasa/openmct/blob/master/API.md#plugins) 162 | 163 | ## Step 2 - Creating a new root node 164 | **Shortcut:** `git checkout -f part-b-step-2` 165 | 166 | To be able to access our spacecraft objects from the tree, we first need to define a root. We will use the Open MCT API to define a new root object representing our spacecraft. 167 | 168 | [dictionary-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-b-step-3/dictionary-plugin.js) 169 | ```javascript 170 | function DictionaryPlugin() { 171 | return function install(openmct) { 172 | openmct.objects.addRoot({ 173 | namespace: 'example.taxonomy', 174 | key: 'spacecraft' 175 | }); 176 | } 177 | }; 178 | ``` 179 | 180 | A new root is added to the object tree using the `addRoot` function exposed by the Open MCT API. `addRoot` accepts an object identifier - defined as a javascript object with a `namespace` and a `key` attribute. [More information on objects and identifiers](https://github.com/nasa/openmct/blob/master/API.md#domain-objects-and-identifiers) is available in our API. 181 | 182 | If we reload the browser now, we should see a new object in the tree. 183 | 184 | ![Open MCT](images/openmct-missing-root.png) 185 | 186 | Currently it will appear as a question mark with `Missing: example.taxonomy:spacecraft` next to it. This is because for now all we've done is provide an identifier for the root node. In the next step, we will define an __Object Provider__, which will provide Open MCT with an object for this identifier. A [basic overview of object providers](https://github.com/nasa/openmct/blob/master/API.md#object-providers) is available in our API documentation. 187 | 188 | ## Step 3 - Providing objects 189 | **Shortcut:** `git checkout -f part-b-step-3` 190 | 191 | Now we will start populating the tree with objects. To do so, we will define an object provider. An Object Provider receives an object identifier, and returns a promise that resolve with an object for the given identifier (if available). In this step we will produce some objects to represent the parts of the spacecraft that produce telemetry data, such as subsystems and instruments. Let's call these telemetry producing things __telemetry points__. Below some code defining and registering an object provider for the new "spacecraft" root object: 192 | 193 | [dictionary-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-b-step-4/dictionary-plugin.js) 194 | ```javascript 195 | function getDictionary() { 196 | return http.get('/dictionary.json') 197 | .then(function (result) { 198 | return result.data; 199 | }); 200 | } 201 | 202 | var objectProvider = { 203 | get: function (identifier) { 204 | return getDictionary().then(function (dictionary) { 205 | if (identifier.key === 'spacecraft') { 206 | return { 207 | identifier: identifier, 208 | name: dictionary.name, 209 | type: 'folder', 210 | location: 'ROOT' 211 | }; 212 | } 213 | }); 214 | } 215 | }; 216 | 217 | function DictionaryPlugin() { 218 | return function install(openmct) { 219 | openmct.objects.addRoot({ 220 | namespace: 'example.taxonomy', 221 | key: 'spacecraft' 222 | }); 223 | 224 | openmct.objects.addProvider('example.taxonomy', objectProvider); 225 | } 226 | }; 227 | ``` 228 | 229 | If we reload our browser now, the unknown object in our tree should be replaced with an object named "Example Spacecraft" with a folder icon. 230 | 231 | ![Open MCT with new Spacecraft root](images/openmct-root-folder.png) 232 | 233 | The root object uses the builtin type `folder`. For the objects representing the telemetry points for our spacecraft, we will now register a new object type. 234 | 235 | Snippet from [dictionary-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-c/dictionary-plugin.js#L63-L67) 236 | ```javascript 237 | openmct.types.addType('example.telemetry', { 238 | name: 'Example Telemetry Point', 239 | description: 'Example telemetry point from our happy tutorial.', 240 | cssClass: 'icon-telemetry' 241 | }); 242 | ``` 243 | 244 | Here we define a new type with a key of `example.telemetry`. For details on the attributes used to specify a new Type, please [see our documentation on object Types](https://github.com/nasa/openmct/blob/master/API.md#domain-object-types) 245 | 246 | Finally, let's modify our object provider to return objects of our newly registered type. Our dictionary plugin will now look like this: 247 | 248 | [dictionary-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-b-step-4/dictionary-plugin.js) 249 | ```javascript 250 | function getDictionary() { 251 | return http.get('/dictionary.json') 252 | .then(function (result) { 253 | return result.data; 254 | }); 255 | } 256 | 257 | var objectProvider = { 258 | get: function (identifier) { 259 | return getDictionary().then(function (dictionary) { 260 | if (identifier.key === 'spacecraft') { 261 | return { 262 | identifier: identifier, 263 | name: dictionary.name, 264 | type: 'folder', 265 | location: 'ROOT' 266 | }; 267 | } else { 268 | var measurement = dictionary.measurements.filter(function (m) { 269 | return m.key === identifier.key; 270 | })[0]; 271 | return { 272 | identifier: identifier, 273 | name: measurement.name, 274 | type: 'example.telemetry', 275 | telemetry: { 276 | values: measurement.values 277 | }, 278 | location: 'example.taxonomy:spacecraft' 279 | }; 280 | } 281 | }); 282 | } 283 | }; 284 | 285 | function DictionaryPlugin() { 286 | return function install(openmct) { 287 | openmct.objects.addRoot({ 288 | namespace: 'example.taxonomy', 289 | key: 'spacecraft' 290 | }); 291 | 292 | openmct.objects.addProvider('example.taxonomy', objectProvider); 293 | 294 | openmct.types.addType('example.telemetry', { 295 | name: 'Example Telemetry Point', 296 | description: 'Example telemetry point from our happy tutorial.', 297 | cssClass: 'icon-telemetry' 298 | }); 299 | } 300 | }; 301 | ``` 302 | 303 | Although we have now defined an Object Provider for both the "Example Spacecraft" and its children (the telemetry measurements), if we refresh our browser at this point we won't see any more objects in the tree. This is because we haven't defined the structure of the tree yet. 304 | 305 | ## Step 4 - Populating the tree 306 | **Shortcut:** `git checkout -f part-b-step-4` 307 | 308 | We have defined a root node in [Step 2](https://github.com/nasa/openmct-tutorial/blob/part-b-step-3/dictionary-plugin.js) and we have provided some objects that will appear in the tree. Now we will provide structure to the tree and define the relationships between objects in the tree. This is achieved with a __[Composition Provider](https://github.com/nasa/openmct/blob/master/API.md#composition-providers)__. 309 | 310 | Snippet from [dictionary-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-c/dictionary-plugin.js#L34-L50) 311 | ```javascript 312 | var compositionProvider = { 313 | appliesTo: function (domainObject) { 314 | return domainObject.identifier.namespace === 'example.taxonomy' && 315 | domainObject.type === 'folder'; 316 | }, 317 | load: function (domainObject) { 318 | return getDictionary() 319 | .then(function (dictionary) { 320 | return dictionary.measurements.map(function (m) { 321 | return { 322 | namespace: 'example.taxonomy', 323 | key: m.key 324 | }; 325 | }); 326 | }); 327 | } 328 | }; 329 | 330 | openmct.composition.addProvider(compositionProvider); 331 | ``` 332 | A Composition Provider accepts a Domain Object, and provides identifiers for the children of that object. For the purposes of this tutorial we will return identifiers for the telemetry points available from our spacecraft. We build these from our spacecraft telemetry dictionary file. 333 | 334 | Our plugin should now look like this - 335 | 336 | [dictionary-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-c/dictionary-plugin.js) 337 | ```javascript 338 | function getDictionary() { 339 | return http.get('/dictionary.json') 340 | .then(function (result) { 341 | return result.data; 342 | }); 343 | } 344 | 345 | var objectProvider = { 346 | get: function (identifier) { 347 | return getDictionary().then(function (dictionary) { 348 | if (identifier.key === 'spacecraft') { 349 | return { 350 | identifier: identifier, 351 | name: dictionary.name, 352 | type: 'folder', 353 | location: 'ROOT' 354 | }; 355 | } else { 356 | var measurement = dictionary.measurements.filter(function (m) { 357 | return m.key === identifier.key; 358 | })[0]; 359 | return { 360 | identifier: identifier, 361 | name: measurement.name, 362 | type: 'example.telemetry', 363 | telemetry: { 364 | values: measurement.values 365 | }, 366 | location: 'example.taxonomy:spacecraft' 367 | }; 368 | } 369 | }); 370 | } 371 | }; 372 | 373 | var compositionProvider = { 374 | appliesTo: function (domainObject) { 375 | return domainObject.identifier.namespace === 'example.taxonomy' && 376 | domainObject.type === 'folder'; 377 | }, 378 | load: function (domainObject) { 379 | return getDictionary() 380 | .then(function (dictionary) { 381 | return dictionary.measurements.map(function (m) { 382 | return { 383 | namespace: 'example.taxonomy', 384 | key: m.key 385 | }; 386 | }); 387 | }); 388 | } 389 | }; 390 | 391 | function DictionaryPlugin() { 392 | return function install(openmct) { 393 | openmct.objects.addRoot({ 394 | namespace: 'example.taxonomy', 395 | key: 'spacecraft' 396 | }); 397 | 398 | openmct.objects.addProvider('example.taxonomy', objectProvider); 399 | 400 | openmct.composition.addProvider(compositionProvider); 401 | 402 | openmct.types.addType('example.telemetry', { 403 | name: 'Example Telemetry Point', 404 | description: 'Example telemetry point from our happy tutorial.', 405 | cssClass: 'icon-telemetry' 406 | }); 407 | }; 408 | }; 409 | ``` 410 | 411 | At this point, if we reload the page we should see a fully populated object tree. 412 | 413 | ![Open MCT with spacecraft telemetry objects](images/telemetry-objects.png) 414 | 415 | Clicking on our telemetry points will display views of those objects, but for now we don't have any telemetry for them. The tutorial telemetry server will provide telemetry for these points, and in the following steps we will define some telemetry adapters to retrieve telemetry data from the server, and provide it to Open MCT. 416 | 417 | # Part C - Integrate/Provide/Request Telemetry 418 | **Shortcut:** `git checkout -f part-c` 419 | 420 | Open MCT supports receiving telemetry by requesting data from a telemetry store, and by subscribing to real-time telemetry updates. In this part of the tutorial we will define and register a telemetry adapter for requesting historical telemetry from our tutorial telemetry server. Let's define our plugin in a new file named `historical-telemetry-plugin.js` 421 | 422 | [historical-telemetry-plugin.js](https://github.com/nasa/openmct-tutorial/blob/part-d/historical-telemetry-plugin.js) 423 | ```javascript 424 | /** 425 | * Basic historical telemetry plugin. 426 | */ 427 | 428 | function HistoricalTelemetryPlugin() { 429 | return function install (openmct) { 430 | var provider = { 431 | supportsRequest: function (domainObject) { 432 | return domainObject.type === 'example.telemetry'; 433 | }, 434 | request: function (domainObject, options) { 435 | var url = '/history/' + 436 | domainObject.identifier.key + 437 | '?start=' + options.start + 438 | '&end=' + options.end; 439 | 440 | return http.get(url) 441 | .then(function (resp) { 442 | return resp.data; 443 | }); 444 | } 445 | }; 446 | 447 | openmct.telemetry.addProvider(provider); 448 | } 449 | } 450 | ``` 451 | 452 | The telemetry adapter above defines two functions. The first of these, `supportsRequest`, is necessary to indicate that this telemetry adapter supports requesting telemetry from a telemetry store. The `request` function will retrieve telemetry data and return it to the Open MCT application for display. 453 | 454 | Our request function also accepts some options. Here we support the specification of a start and end date. 455 | 456 | With our adapter defined, we need to update `index.html` to include it. 457 | 458 | [index.html](https://github.com/nasa/openmct-tutorial/blob/part-d/index.html) 459 | ```html 460 | 461 | 462 | 463 | Open MCT Tutorials 464 | 465 | 466 | 467 | 468 | 485 | 486 | 487 |
488 | 489 | 490 | 491 | ``` 492 | 493 | At this point If we refresh the page we should now see some telemetry for our telemetry points. For example, navigating to the "Generator Temperature" telemetry point should show us a plot of the telemetry generated since the server started running. 494 | 495 | # Part D - Subscribing to New Telemetry 496 | **Shortcut:** `git checkout -f part-d` 497 | 498 | We are now going to define a telemetry adapter that allows Open MCT to subscribe to our tutorial server for new telemetry as it becomes available. The process of defining a telemetry adapter for subscribing to real-time telemetry is similar to our previously defined historical telemetry adapter, except that we define a `supportsSubscribe` function to indicate that this adapter provides telemetry subscriptions, and a `subscribe` function for subscribing to updates. This adapter uses a simple messaging system for subscribing to telemetry updates over a websocket. 499 | 500 | Let's define our new plugin in a file named `realtime-telemetry-plugin.js`. 501 | 502 | [realtime-telemetry-plugin.js](https://github.com/nasa/openmct-tutorial/blob/master/realtime-telemetry-plugin.js) 503 | ```javascript 504 | /** 505 | * Basic Realtime telemetry plugin using websockets. 506 | */ 507 | function RealtimeTelemetryPlugin() { 508 | return function (openmct) { 509 | var socket = new WebSocket(location.origin.replace(/^http/, 'ws') + '/realtime/'); 510 | var listener = {}; 511 | 512 | socket.onmessage = function (event) { 513 | point = JSON.parse(event.data); 514 | if (listener[point.id]) { 515 | listener[point.id](point); 516 | } 517 | }; 518 | 519 | var provider = { 520 | supportsSubscribe: function (domainObject) { 521 | return domainObject.type === 'example.telemetry'; 522 | }, 523 | subscribe: function (domainObject, callback) { 524 | listener[domainObject.identifier.key] = callback; 525 | socket.send('subscribe ' + domainObject.identifier.key); 526 | return function unsubscribe() { 527 | delete listener[domainObject.identifier.key]; 528 | socket.send('unsubscribe ' + domainObject.identifier.key); 529 | }; 530 | } 531 | }; 532 | 533 | openmct.telemetry.addProvider(provider); 534 | } 535 | } 536 | ``` 537 | 538 | The subscribe function accepts as arguments the Domain Object for which we are interested in telemetry, and a callback function. The callback function will be invoked with telemetry data as they become available. 539 | 540 | With our realtime telemetry plugin defined, let's include it from `index.html`. 541 | 542 | [index.html](https://github.com/nasa/openmct-tutorial/blob/master/index.html) 543 | ```html 544 | 545 | 546 | 547 | Open MCT Tutorials 548 | 549 | 550 | 551 | 552 | 553 | 571 | 572 | 573 |
574 | 575 | 576 | ``` 577 | 578 | If we refresh the page and navigate to one of our telemetry points we should now see telemetry flowing. For example, navigating to the "Generator Temperature" telemetry point should show us a plot of telemetry data that is now updated regularly. 579 | -------------------------------------------------------------------------------- /dictionary-plugin.js: -------------------------------------------------------------------------------- 1 | function getDictionary() { 2 | return http.get('/dictionary.json') 3 | .then(function (result) { 4 | return result.data; 5 | }); 6 | } 7 | 8 | var objectProvider = { 9 | get: function (identifier) { 10 | return getDictionary().then(function (dictionary) { 11 | if (identifier.key === 'spacecraft') { 12 | return { 13 | identifier: identifier, 14 | name: dictionary.name, 15 | type: 'folder', 16 | location: 'ROOT' 17 | }; 18 | } else { 19 | var measurement = dictionary.measurements.filter(function (m) { 20 | return m.key === identifier.key; 21 | })[0]; 22 | return { 23 | identifier: identifier, 24 | name: measurement.name, 25 | type: 'example.telemetry', 26 | telemetry: { 27 | values: measurement.values 28 | }, 29 | location: 'example.taxonomy:spacecraft' 30 | }; 31 | } 32 | }); 33 | } 34 | }; 35 | 36 | var compositionProvider = { 37 | appliesTo: function (domainObject) { 38 | return domainObject.identifier.namespace === 'example.taxonomy' && 39 | domainObject.type === 'folder'; 40 | }, 41 | load: function (domainObject) { 42 | return getDictionary() 43 | .then(function (dictionary) { 44 | return dictionary.measurements.map(function (m) { 45 | return { 46 | namespace: 'example.taxonomy', 47 | key: m.key 48 | }; 49 | }); 50 | }); 51 | } 52 | }; 53 | 54 | var DictionaryPlugin = function (openmct) { 55 | return function install(openmct) { 56 | openmct.objects.addRoot({ 57 | namespace: 'example.taxonomy', 58 | key: 'spacecraft' 59 | }); 60 | 61 | openmct.objects.addProvider('example.taxonomy', objectProvider); 62 | 63 | openmct.composition.addProvider(compositionProvider); 64 | 65 | openmct.types.addType('example.telemetry', { 66 | name: 'Example Telemetry Point', 67 | description: 'Example telemetry point from our happy tutorial.', 68 | cssClass: 'icon-telemetry' 69 | }); 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /dictionary.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example Spacecraft", 3 | "key": "sc", 4 | "measurements": [ 5 | { 6 | "name": "Fuel", 7 | "key": "prop.fuel", 8 | "values": [ 9 | { 10 | "key": "value", 11 | "name": "Value", 12 | "units": "kilograms", 13 | "format": "float", 14 | "min": 0, 15 | "max": 100, 16 | "hints": { 17 | "range": 1 18 | } 19 | }, 20 | { 21 | "key": "utc", 22 | "source": "timestamp", 23 | "name": "Timestamp", 24 | "format": "utc", 25 | "hints": { 26 | "domain": 1 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "Thrusters", 33 | "key": "prop.thrusters", 34 | "values": [ 35 | { 36 | "key": "value", 37 | "name": "Value", 38 | "format": "enum", 39 | "enumerations": [ 40 | { 41 | "string": "ON", 42 | "value": 1 43 | }, 44 | { 45 | "string": "OFF", 46 | "value": 0 47 | } 48 | ], 49 | "hints": { 50 | "range": 1 51 | } 52 | }, 53 | { 54 | "key": "utc", 55 | "source": "timestamp", 56 | "name": "Timestamp", 57 | "format": "utc", 58 | "hints": { 59 | "domain": 1 60 | } 61 | } 62 | ] 63 | }, 64 | { 65 | "name": "Received", 66 | "key": "comms.recd", 67 | "values": [ 68 | { 69 | "key": "value", 70 | "name": "Value", 71 | "units": "bytes", 72 | "format": "integer", 73 | "hints": { 74 | "range": 1 75 | } 76 | }, 77 | { 78 | "key": "utc", 79 | "source": "timestamp", 80 | "name": "Timestamp", 81 | "format": "utc", 82 | "hints": { 83 | "domain": 1 84 | } 85 | } 86 | ] 87 | }, 88 | { 89 | "name": "Sent", 90 | "key": "comms.sent", 91 | "values": [ 92 | { 93 | "key": "value", 94 | "name": "Value", 95 | "units": "bytes", 96 | "format": "integer", 97 | "hints": { 98 | "range": 1 99 | } 100 | }, 101 | { 102 | "key": "utc", 103 | "source": "timestamp", 104 | "name": "Timestamp", 105 | "format": "utc", 106 | "hints": { 107 | "domain": 1 108 | } 109 | } 110 | ] 111 | }, 112 | { 113 | "name": "Generator Temperature", 114 | "key": "pwr.temp", 115 | "values": [ 116 | { 117 | "key": "value", 118 | "name": "Value", 119 | "units": "℃", 120 | "format": "float", 121 | "hints": { 122 | "range": 1 123 | } 124 | }, 125 | { 126 | "key": "utc", 127 | "source": "timestamp", 128 | "name": "Timestamp", 129 | "format": "utc", 130 | "hints": { 131 | "domain": 1 132 | } 133 | } 134 | ] 135 | }, 136 | { 137 | "name": "Generator Current", 138 | "key": "pwr.c", 139 | "values": [ 140 | { 141 | "key": "value", 142 | "name": "Value", 143 | "units": "Amps", 144 | "format": "float", 145 | "hints": { 146 | "range": 1 147 | } 148 | }, 149 | { 150 | "key": "utc", 151 | "source": "timestamp", 152 | "name": "Timestamp", 153 | "format": "utc", 154 | "hints": { 155 | "domain": 1 156 | } 157 | } 158 | ] 159 | }, 160 | { 161 | "name": "Generator Voltage", 162 | "key": "pwr.v", 163 | "values": [ 164 | { 165 | "key": "value", 166 | "name": "Value", 167 | "units": "Volts", 168 | "format": "float", 169 | "hints": { 170 | "range": 1 171 | } 172 | }, 173 | { 174 | "key": "utc", 175 | "source": "timestamp", 176 | "name": "Timestamp", 177 | "format": "utc", 178 | "hints": { 179 | "domain": 1 180 | } 181 | } 182 | ] 183 | } 184 | ] 185 | } 186 | -------------------------------------------------------------------------------- /example-server/history-server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | function HistoryServer(spacecraft) { 4 | var router = express.Router(); 5 | 6 | router.get('/:pointId', function (req, res) { 7 | var start = +req.query.start; 8 | var end = +req.query.end; 9 | var ids = req.params.pointId.split(','); 10 | 11 | var response = ids.reduce(function (resp, id) { 12 | return resp.concat(spacecraft.history[id].filter(function (p) { 13 | return p.timestamp > start && p.timestamp < end; 14 | })); 15 | }, []); 16 | res.status(200).json(response).end(); 17 | }); 18 | 19 | return router; 20 | } 21 | 22 | module.exports = HistoryServer; 23 | 24 | -------------------------------------------------------------------------------- /example-server/realtime-server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | function RealtimeServer(spacecraft) { 4 | 5 | var router = express.Router(); 6 | 7 | router.ws('/', function (ws) { 8 | var unlisten = spacecraft.listen(notifySubscribers); 9 | var subscribed = {}; // Active subscriptions for this connection 10 | var handlers = { // Handlers for specific requests 11 | subscribe: function (id) { 12 | subscribed[id] = true; 13 | }, 14 | unsubscribe: function (id) { 15 | delete subscribed[id]; 16 | } 17 | }; 18 | 19 | function notifySubscribers(point) { 20 | if (subscribed[point.id]) { 21 | ws.send(JSON.stringify(point)); 22 | } 23 | } 24 | 25 | // Listen for requests 26 | ws.on('message', function (message) { 27 | var parts = message.split(' '), 28 | handler = handlers[parts[0]]; 29 | if (handler) { 30 | handler.apply(handlers, parts.slice(1)); 31 | } 32 | }); 33 | 34 | // Stop sending telemetry updates for this connection when closed 35 | ws.on('close', unlisten); 36 | }); 37 | 38 | return router; 39 | }; 40 | 41 | module.exports = RealtimeServer; 42 | -------------------------------------------------------------------------------- /example-server/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic implementation of a history and realtime server. 3 | */ 4 | 5 | var Spacecraft = require('./spacecraft'); 6 | var RealtimeServer = require('./realtime-server'); 7 | var HistoryServer = require('./history-server'); 8 | var StaticServer = require('./static-server'); 9 | 10 | var expressWs = require('express-ws'); 11 | var app = require('express')(); 12 | expressWs(app); 13 | 14 | var spacecraft = new Spacecraft(); 15 | var realtimeServer = new RealtimeServer(spacecraft); 16 | var historyServer = new HistoryServer(spacecraft); 17 | var staticServer = new StaticServer(); 18 | 19 | app.use('/realtime', realtimeServer); 20 | app.use('/history', historyServer); 21 | app.use('/', staticServer); 22 | 23 | var port = process.env.PORT || 8080 24 | 25 | app.listen(port, function () { 26 | console.log('Open MCT hosted at http://localhost:' + port); 27 | console.log('History hosted at http://localhost:' + port + '/history'); 28 | console.log('Realtime hosted at ws://localhost:' + port + '/realtime'); 29 | }); 30 | -------------------------------------------------------------------------------- /example-server/spacecraft.js: -------------------------------------------------------------------------------- 1 | /* 2 | Spacecraft.js simulates a small spacecraft generating telemetry. 3 | */ 4 | 5 | function Spacecraft() { 6 | this.state = { 7 | "prop.fuel": 77, 8 | "prop.thrusters": "OFF", 9 | "comms.recd": 0, 10 | "comms.sent": 0, 11 | "pwr.temp": 245, 12 | "pwr.c": 8.15, 13 | "pwr.v": 30 14 | }; 15 | this.history = {}; 16 | this.listeners = []; 17 | Object.keys(this.state).forEach(function (k) { 18 | this.history[k] = []; 19 | }, this); 20 | 21 | setInterval(function () { 22 | this.updateState(); 23 | this.generateTelemetry(); 24 | }.bind(this), 1000); 25 | 26 | console.log("Example spacecraft launched!"); 27 | console.log("Press Enter to toggle thruster state."); 28 | 29 | process.stdin.on('data', function () { 30 | this.state['prop.thrusters'] = 31 | (this.state['prop.thrusters'] === "OFF") ? "ON" : "OFF"; 32 | this.state['comms.recd'] += 32; 33 | console.log("Thrusters " + this.state["prop.thrusters"]); 34 | this.generateTelemetry(); 35 | }.bind(this)); 36 | }; 37 | 38 | Spacecraft.prototype.updateState = function () { 39 | this.state["prop.fuel"] = Math.max( 40 | 0, 41 | this.state["prop.fuel"] - 42 | (this.state["prop.thrusters"] === "ON" ? 0.5 : 0) 43 | ); 44 | this.state["pwr.temp"] = this.state["pwr.temp"] * 0.985 45 | + Math.random() * 0.25 + Math.sin(Date.now()); 46 | if (this.state["prop.thrusters"] === "ON") { 47 | this.state["pwr.c"] = 8.15; 48 | } else { 49 | this.state["pwr.c"] = this.state["pwr.c"] * 0.985; 50 | } 51 | this.state["pwr.v"] = 30 + Math.pow(Math.random(), 3); 52 | }; 53 | 54 | /** 55 | * Takes a measurement of spacecraft state, stores in history, and notifies 56 | * listeners. 57 | */ 58 | Spacecraft.prototype.generateTelemetry = function () { 59 | var timestamp = Date.now(), sent = 0; 60 | Object.keys(this.state).forEach(function (id) { 61 | var state = { timestamp: timestamp, value: this.state[id], id: id}; 62 | this.notify(state); 63 | this.history[id].push(state); 64 | this.state["comms.sent"] += JSON.stringify(state).length; 65 | }, this); 66 | }; 67 | 68 | Spacecraft.prototype.notify = function (point) { 69 | this.listeners.forEach(function (l) { 70 | l(point); 71 | }); 72 | }; 73 | 74 | Spacecraft.prototype.listen = function (listener) { 75 | this.listeners.push(listener); 76 | return function () { 77 | this.listeners = this.listeners.filter(function (l) { 78 | return l !== listener; 79 | }); 80 | }.bind(this); 81 | }; 82 | 83 | module.exports = function () { 84 | return new Spacecraft() 85 | }; -------------------------------------------------------------------------------- /example-server/static-server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | function StaticServer() { 4 | var router = express.Router(); 5 | 6 | router.use('/', express.static(__dirname + '/..')); 7 | 8 | return router 9 | } 10 | 11 | module.exports = StaticServer; 12 | -------------------------------------------------------------------------------- /historical-telemetry-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic historical telemetry plugin. 3 | */ 4 | 5 | function HistoricalTelemetryPlugin() { 6 | return function install (openmct) { 7 | var provider = { 8 | supportsRequest: function (domainObject) { 9 | return domainObject.type === 'example.telemetry'; 10 | }, 11 | request: function (domainObject, options) { 12 | var url = '/history/' + 13 | domainObject.identifier.key + 14 | '?start=' + options.start + 15 | '&end=' + options.end; 16 | 17 | return http.get(url) 18 | .then(function (resp) { 19 | return resp.data; 20 | }); 21 | } 22 | }; 23 | 24 | openmct.telemetry.addProvider(provider); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /images/object-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/openmct-tutorial/a6429d0147a05112e1a2ffd03046a0e5307d7e0a/images/object-tree.png -------------------------------------------------------------------------------- /images/openmct-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/openmct-tutorial/a6429d0147a05112e1a2ffd03046a0e5307d7e0a/images/openmct-empty.png -------------------------------------------------------------------------------- /images/openmct-missing-root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/openmct-tutorial/a6429d0147a05112e1a2ffd03046a0e5307d7e0a/images/openmct-missing-root.png -------------------------------------------------------------------------------- /images/openmct-root-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/openmct-tutorial/a6429d0147a05112e1a2ffd03046a0e5307d7e0a/images/openmct-root-folder.png -------------------------------------------------------------------------------- /images/telemetry-objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/openmct-tutorial/a6429d0147a05112e1a2ffd03046a0e5307d7e0a/images/telemetry-objects.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Open MCT Tutorials 5 | 6 | 7 | 8 | 9 | 10 | 27 | 28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/http.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory; 6 | } else { 7 | root.http = factory(root); 8 | } 9 | })(this, function (root) { 10 | 11 | 'use strict'; 12 | 13 | var exports = {}; 14 | 15 | var generateResponse = function (req) { 16 | var response = { 17 | data: req.responseText, 18 | status: req.status, 19 | request: req 20 | }; 21 | if (req.getResponseHeader('Content-Type').indexOf('application/json') !== -1) { 22 | response.data = JSON.parse(response.data); 23 | } 24 | return response; 25 | }; 26 | 27 | var xhr = function (type, url, data) { 28 | var promise = new Promise(function (resolve, reject) { 29 | var XHR = XMLHttpRequest || ActiveXObject; 30 | var request = new XHR('MSXML2.XMLHTTP.3.0'); 31 | 32 | request.open(type, url, true); 33 | request.onreadystatechange = function () { 34 | var req; 35 | if (request.readyState === 4) { 36 | req = generateResponse(request); 37 | if (request.status >= 200 && request.status < 300) { 38 | resolve(req); 39 | } else { 40 | reject(req); 41 | } 42 | } 43 | }; 44 | request.send(data); 45 | }); 46 | return promise; 47 | }; 48 | 49 | exports.get = function (src) { 50 | return xhr('GET', src); 51 | }; 52 | 53 | exports.put = function (url, data) { 54 | return xhr('PUT', url, data); 55 | }; 56 | 57 | exports.post= function (url, data) { 58 | return xhr('POST', url, data); 59 | }; 60 | 61 | exports.delete = function (url) { 62 | return xhr('DELETE', url); 63 | }; 64 | 65 | return exports; 66 | }); 67 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openmct-tutorials", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "openmct-tutorials", 9 | "version": "0.0.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.19.2", 13 | "express-ws": "^4.0.0", 14 | "openmct": "latest", 15 | "ws": "^6.1.2" 16 | } 17 | }, 18 | "node_modules/accepts": { 19 | "version": "1.3.8", 20 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 21 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 22 | "dependencies": { 23 | "mime-types": "~2.1.34", 24 | "negotiator": "0.6.3" 25 | }, 26 | "engines": { 27 | "node": ">= 0.6" 28 | } 29 | }, 30 | "node_modules/array-flatten": { 31 | "version": "1.1.1", 32 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 33 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 34 | }, 35 | "node_modules/async-limiter": { 36 | "version": "1.0.1", 37 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 38 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 39 | }, 40 | "node_modules/body-parser": { 41 | "version": "1.20.2", 42 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", 43 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", 44 | "dependencies": { 45 | "bytes": "3.1.2", 46 | "content-type": "~1.0.5", 47 | "debug": "2.6.9", 48 | "depd": "2.0.0", 49 | "destroy": "1.2.0", 50 | "http-errors": "2.0.0", 51 | "iconv-lite": "0.4.24", 52 | "on-finished": "2.4.1", 53 | "qs": "6.11.0", 54 | "raw-body": "2.5.2", 55 | "type-is": "~1.6.18", 56 | "unpipe": "1.0.0" 57 | }, 58 | "engines": { 59 | "node": ">= 0.8", 60 | "npm": "1.2.8000 || >= 1.4.16" 61 | } 62 | }, 63 | "node_modules/bytes": { 64 | "version": "3.1.2", 65 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 66 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 67 | "engines": { 68 | "node": ">= 0.8" 69 | } 70 | }, 71 | "node_modules/call-bind": { 72 | "version": "1.0.7", 73 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 74 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 75 | "dependencies": { 76 | "es-define-property": "^1.0.0", 77 | "es-errors": "^1.3.0", 78 | "function-bind": "^1.1.2", 79 | "get-intrinsic": "^1.2.4", 80 | "set-function-length": "^1.2.1" 81 | }, 82 | "engines": { 83 | "node": ">= 0.4" 84 | }, 85 | "funding": { 86 | "url": "https://github.com/sponsors/ljharb" 87 | } 88 | }, 89 | "node_modules/content-disposition": { 90 | "version": "0.5.4", 91 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 92 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 93 | "dependencies": { 94 | "safe-buffer": "5.2.1" 95 | }, 96 | "engines": { 97 | "node": ">= 0.6" 98 | } 99 | }, 100 | "node_modules/content-type": { 101 | "version": "1.0.5", 102 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 103 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 104 | "engines": { 105 | "node": ">= 0.6" 106 | } 107 | }, 108 | "node_modules/cookie": { 109 | "version": "0.6.0", 110 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 111 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 112 | "engines": { 113 | "node": ">= 0.6" 114 | } 115 | }, 116 | "node_modules/cookie-signature": { 117 | "version": "1.0.6", 118 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 119 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 120 | }, 121 | "node_modules/debug": { 122 | "version": "2.6.9", 123 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 124 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 125 | "dependencies": { 126 | "ms": "2.0.0" 127 | } 128 | }, 129 | "node_modules/define-data-property": { 130 | "version": "1.1.4", 131 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 132 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 133 | "dependencies": { 134 | "es-define-property": "^1.0.0", 135 | "es-errors": "^1.3.0", 136 | "gopd": "^1.0.1" 137 | }, 138 | "engines": { 139 | "node": ">= 0.4" 140 | }, 141 | "funding": { 142 | "url": "https://github.com/sponsors/ljharb" 143 | } 144 | }, 145 | "node_modules/depd": { 146 | "version": "2.0.0", 147 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 148 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 149 | "engines": { 150 | "node": ">= 0.8" 151 | } 152 | }, 153 | "node_modules/destroy": { 154 | "version": "1.2.0", 155 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 156 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 157 | "engines": { 158 | "node": ">= 0.8", 159 | "npm": "1.2.8000 || >= 1.4.16" 160 | } 161 | }, 162 | "node_modules/ee-first": { 163 | "version": "1.1.1", 164 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 165 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 166 | }, 167 | "node_modules/encodeurl": { 168 | "version": "1.0.2", 169 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 170 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 171 | "engines": { 172 | "node": ">= 0.8" 173 | } 174 | }, 175 | "node_modules/es-define-property": { 176 | "version": "1.0.0", 177 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 178 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 179 | "dependencies": { 180 | "get-intrinsic": "^1.2.4" 181 | }, 182 | "engines": { 183 | "node": ">= 0.4" 184 | } 185 | }, 186 | "node_modules/es-errors": { 187 | "version": "1.3.0", 188 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 189 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 190 | "engines": { 191 | "node": ">= 0.4" 192 | } 193 | }, 194 | "node_modules/escape-html": { 195 | "version": "1.0.3", 196 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 197 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 198 | }, 199 | "node_modules/etag": { 200 | "version": "1.8.1", 201 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 202 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 203 | "engines": { 204 | "node": ">= 0.6" 205 | } 206 | }, 207 | "node_modules/express": { 208 | "version": "4.19.2", 209 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", 210 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", 211 | "dependencies": { 212 | "accepts": "~1.3.8", 213 | "array-flatten": "1.1.1", 214 | "body-parser": "1.20.2", 215 | "content-disposition": "0.5.4", 216 | "content-type": "~1.0.4", 217 | "cookie": "0.6.0", 218 | "cookie-signature": "1.0.6", 219 | "debug": "2.6.9", 220 | "depd": "2.0.0", 221 | "encodeurl": "~1.0.2", 222 | "escape-html": "~1.0.3", 223 | "etag": "~1.8.1", 224 | "finalhandler": "1.2.0", 225 | "fresh": "0.5.2", 226 | "http-errors": "2.0.0", 227 | "merge-descriptors": "1.0.1", 228 | "methods": "~1.1.2", 229 | "on-finished": "2.4.1", 230 | "parseurl": "~1.3.3", 231 | "path-to-regexp": "0.1.7", 232 | "proxy-addr": "~2.0.7", 233 | "qs": "6.11.0", 234 | "range-parser": "~1.2.1", 235 | "safe-buffer": "5.2.1", 236 | "send": "0.18.0", 237 | "serve-static": "1.15.0", 238 | "setprototypeof": "1.2.0", 239 | "statuses": "2.0.1", 240 | "type-is": "~1.6.18", 241 | "utils-merge": "1.0.1", 242 | "vary": "~1.1.2" 243 | }, 244 | "engines": { 245 | "node": ">= 0.10.0" 246 | } 247 | }, 248 | "node_modules/express-ws": { 249 | "version": "4.0.0", 250 | "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz", 251 | "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==", 252 | "dependencies": { 253 | "ws": "^5.2.0" 254 | }, 255 | "engines": { 256 | "node": ">=4.5.0" 257 | }, 258 | "peerDependencies": { 259 | "express": "^4.0.0 || ^5.0.0-alpha.1" 260 | } 261 | }, 262 | "node_modules/express-ws/node_modules/ws": { 263 | "version": "5.2.3", 264 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", 265 | "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", 266 | "dependencies": { 267 | "async-limiter": "~1.0.0" 268 | } 269 | }, 270 | "node_modules/finalhandler": { 271 | "version": "1.2.0", 272 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 273 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 274 | "dependencies": { 275 | "debug": "2.6.9", 276 | "encodeurl": "~1.0.2", 277 | "escape-html": "~1.0.3", 278 | "on-finished": "2.4.1", 279 | "parseurl": "~1.3.3", 280 | "statuses": "2.0.1", 281 | "unpipe": "~1.0.0" 282 | }, 283 | "engines": { 284 | "node": ">= 0.8" 285 | } 286 | }, 287 | "node_modules/forwarded": { 288 | "version": "0.2.0", 289 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 290 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 291 | "engines": { 292 | "node": ">= 0.6" 293 | } 294 | }, 295 | "node_modules/fresh": { 296 | "version": "0.5.2", 297 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 298 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 299 | "engines": { 300 | "node": ">= 0.6" 301 | } 302 | }, 303 | "node_modules/function-bind": { 304 | "version": "1.1.2", 305 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 306 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 307 | "funding": { 308 | "url": "https://github.com/sponsors/ljharb" 309 | } 310 | }, 311 | "node_modules/get-intrinsic": { 312 | "version": "1.2.4", 313 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 314 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 315 | "dependencies": { 316 | "es-errors": "^1.3.0", 317 | "function-bind": "^1.1.2", 318 | "has-proto": "^1.0.1", 319 | "has-symbols": "^1.0.3", 320 | "hasown": "^2.0.0" 321 | }, 322 | "engines": { 323 | "node": ">= 0.4" 324 | }, 325 | "funding": { 326 | "url": "https://github.com/sponsors/ljharb" 327 | } 328 | }, 329 | "node_modules/gopd": { 330 | "version": "1.0.1", 331 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 332 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 333 | "dependencies": { 334 | "get-intrinsic": "^1.1.3" 335 | }, 336 | "funding": { 337 | "url": "https://github.com/sponsors/ljharb" 338 | } 339 | }, 340 | "node_modules/has-property-descriptors": { 341 | "version": "1.0.2", 342 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 343 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 344 | "dependencies": { 345 | "es-define-property": "^1.0.0" 346 | }, 347 | "funding": { 348 | "url": "https://github.com/sponsors/ljharb" 349 | } 350 | }, 351 | "node_modules/has-proto": { 352 | "version": "1.0.3", 353 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 354 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 355 | "engines": { 356 | "node": ">= 0.4" 357 | }, 358 | "funding": { 359 | "url": "https://github.com/sponsors/ljharb" 360 | } 361 | }, 362 | "node_modules/has-symbols": { 363 | "version": "1.0.3", 364 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 365 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 366 | "engines": { 367 | "node": ">= 0.4" 368 | }, 369 | "funding": { 370 | "url": "https://github.com/sponsors/ljharb" 371 | } 372 | }, 373 | "node_modules/hasown": { 374 | "version": "2.0.2", 375 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 376 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 377 | "dependencies": { 378 | "function-bind": "^1.1.2" 379 | }, 380 | "engines": { 381 | "node": ">= 0.4" 382 | } 383 | }, 384 | "node_modules/http-errors": { 385 | "version": "2.0.0", 386 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 387 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 388 | "dependencies": { 389 | "depd": "2.0.0", 390 | "inherits": "2.0.4", 391 | "setprototypeof": "1.2.0", 392 | "statuses": "2.0.1", 393 | "toidentifier": "1.0.1" 394 | }, 395 | "engines": { 396 | "node": ">= 0.8" 397 | } 398 | }, 399 | "node_modules/iconv-lite": { 400 | "version": "0.4.24", 401 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 402 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 403 | "dependencies": { 404 | "safer-buffer": ">= 2.1.2 < 3" 405 | }, 406 | "engines": { 407 | "node": ">=0.10.0" 408 | } 409 | }, 410 | "node_modules/inherits": { 411 | "version": "2.0.4", 412 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 413 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 414 | }, 415 | "node_modules/ipaddr.js": { 416 | "version": "1.9.1", 417 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 418 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 419 | "engines": { 420 | "node": ">= 0.10" 421 | } 422 | }, 423 | "node_modules/media-typer": { 424 | "version": "0.3.0", 425 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 426 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 427 | "engines": { 428 | "node": ">= 0.6" 429 | } 430 | }, 431 | "node_modules/merge-descriptors": { 432 | "version": "1.0.1", 433 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 434 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 435 | }, 436 | "node_modules/methods": { 437 | "version": "1.1.2", 438 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 439 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 440 | "engines": { 441 | "node": ">= 0.6" 442 | } 443 | }, 444 | "node_modules/mime": { 445 | "version": "1.6.0", 446 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 447 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 448 | "bin": { 449 | "mime": "cli.js" 450 | }, 451 | "engines": { 452 | "node": ">=4" 453 | } 454 | }, 455 | "node_modules/mime-db": { 456 | "version": "1.52.0", 457 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 458 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 459 | "engines": { 460 | "node": ">= 0.6" 461 | } 462 | }, 463 | "node_modules/mime-types": { 464 | "version": "2.1.35", 465 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 466 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 467 | "dependencies": { 468 | "mime-db": "1.52.0" 469 | }, 470 | "engines": { 471 | "node": ">= 0.6" 472 | } 473 | }, 474 | "node_modules/ms": { 475 | "version": "2.0.0", 476 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 477 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 478 | }, 479 | "node_modules/negotiator": { 480 | "version": "0.6.3", 481 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 482 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 483 | "engines": { 484 | "node": ">= 0.6" 485 | } 486 | }, 487 | "node_modules/object-inspect": { 488 | "version": "1.13.1", 489 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 490 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 491 | "funding": { 492 | "url": "https://github.com/sponsors/ljharb" 493 | } 494 | }, 495 | "node_modules/on-finished": { 496 | "version": "2.4.1", 497 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 498 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 499 | "dependencies": { 500 | "ee-first": "1.1.1" 501 | }, 502 | "engines": { 503 | "node": ">= 0.8" 504 | } 505 | }, 506 | "node_modules/openmct": { 507 | "version": "3.2.1", 508 | "resolved": "https://registry.npmjs.org/openmct/-/openmct-3.2.1.tgz", 509 | "integrity": "sha512-BxCI8ImYSangOBu8upGuj+R9ZUDwaffbezC2WlH5ZGqRLi0vN+0AYIx1DS+21012aBupophM40upCEPcQN7Zsg==", 510 | "engines": { 511 | "node": ">=16.19.1 <20" 512 | } 513 | }, 514 | "node_modules/parseurl": { 515 | "version": "1.3.3", 516 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 517 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 518 | "engines": { 519 | "node": ">= 0.8" 520 | } 521 | }, 522 | "node_modules/path-to-regexp": { 523 | "version": "0.1.7", 524 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 525 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 526 | }, 527 | "node_modules/proxy-addr": { 528 | "version": "2.0.7", 529 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 530 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 531 | "dependencies": { 532 | "forwarded": "0.2.0", 533 | "ipaddr.js": "1.9.1" 534 | }, 535 | "engines": { 536 | "node": ">= 0.10" 537 | } 538 | }, 539 | "node_modules/qs": { 540 | "version": "6.11.0", 541 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 542 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 543 | "dependencies": { 544 | "side-channel": "^1.0.4" 545 | }, 546 | "engines": { 547 | "node": ">=0.6" 548 | }, 549 | "funding": { 550 | "url": "https://github.com/sponsors/ljharb" 551 | } 552 | }, 553 | "node_modules/range-parser": { 554 | "version": "1.2.1", 555 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 556 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 557 | "engines": { 558 | "node": ">= 0.6" 559 | } 560 | }, 561 | "node_modules/raw-body": { 562 | "version": "2.5.2", 563 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 564 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 565 | "dependencies": { 566 | "bytes": "3.1.2", 567 | "http-errors": "2.0.0", 568 | "iconv-lite": "0.4.24", 569 | "unpipe": "1.0.0" 570 | }, 571 | "engines": { 572 | "node": ">= 0.8" 573 | } 574 | }, 575 | "node_modules/safe-buffer": { 576 | "version": "5.2.1", 577 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 578 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 579 | "funding": [ 580 | { 581 | "type": "github", 582 | "url": "https://github.com/sponsors/feross" 583 | }, 584 | { 585 | "type": "patreon", 586 | "url": "https://www.patreon.com/feross" 587 | }, 588 | { 589 | "type": "consulting", 590 | "url": "https://feross.org/support" 591 | } 592 | ] 593 | }, 594 | "node_modules/safer-buffer": { 595 | "version": "2.1.2", 596 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 597 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 598 | }, 599 | "node_modules/send": { 600 | "version": "0.18.0", 601 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 602 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 603 | "dependencies": { 604 | "debug": "2.6.9", 605 | "depd": "2.0.0", 606 | "destroy": "1.2.0", 607 | "encodeurl": "~1.0.2", 608 | "escape-html": "~1.0.3", 609 | "etag": "~1.8.1", 610 | "fresh": "0.5.2", 611 | "http-errors": "2.0.0", 612 | "mime": "1.6.0", 613 | "ms": "2.1.3", 614 | "on-finished": "2.4.1", 615 | "range-parser": "~1.2.1", 616 | "statuses": "2.0.1" 617 | }, 618 | "engines": { 619 | "node": ">= 0.8.0" 620 | } 621 | }, 622 | "node_modules/send/node_modules/ms": { 623 | "version": "2.1.3", 624 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 625 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 626 | }, 627 | "node_modules/serve-static": { 628 | "version": "1.15.0", 629 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 630 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 631 | "dependencies": { 632 | "encodeurl": "~1.0.2", 633 | "escape-html": "~1.0.3", 634 | "parseurl": "~1.3.3", 635 | "send": "0.18.0" 636 | }, 637 | "engines": { 638 | "node": ">= 0.8.0" 639 | } 640 | }, 641 | "node_modules/set-function-length": { 642 | "version": "1.2.2", 643 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 644 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 645 | "dependencies": { 646 | "define-data-property": "^1.1.4", 647 | "es-errors": "^1.3.0", 648 | "function-bind": "^1.1.2", 649 | "get-intrinsic": "^1.2.4", 650 | "gopd": "^1.0.1", 651 | "has-property-descriptors": "^1.0.2" 652 | }, 653 | "engines": { 654 | "node": ">= 0.4" 655 | } 656 | }, 657 | "node_modules/setprototypeof": { 658 | "version": "1.2.0", 659 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 660 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 661 | }, 662 | "node_modules/side-channel": { 663 | "version": "1.0.6", 664 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 665 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 666 | "dependencies": { 667 | "call-bind": "^1.0.7", 668 | "es-errors": "^1.3.0", 669 | "get-intrinsic": "^1.2.4", 670 | "object-inspect": "^1.13.1" 671 | }, 672 | "engines": { 673 | "node": ">= 0.4" 674 | }, 675 | "funding": { 676 | "url": "https://github.com/sponsors/ljharb" 677 | } 678 | }, 679 | "node_modules/statuses": { 680 | "version": "2.0.1", 681 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 682 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 683 | "engines": { 684 | "node": ">= 0.8" 685 | } 686 | }, 687 | "node_modules/toidentifier": { 688 | "version": "1.0.1", 689 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 690 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 691 | "engines": { 692 | "node": ">=0.6" 693 | } 694 | }, 695 | "node_modules/type-is": { 696 | "version": "1.6.18", 697 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 698 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 699 | "dependencies": { 700 | "media-typer": "0.3.0", 701 | "mime-types": "~2.1.24" 702 | }, 703 | "engines": { 704 | "node": ">= 0.6" 705 | } 706 | }, 707 | "node_modules/unpipe": { 708 | "version": "1.0.0", 709 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 710 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 711 | "engines": { 712 | "node": ">= 0.8" 713 | } 714 | }, 715 | "node_modules/utils-merge": { 716 | "version": "1.0.1", 717 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 718 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 719 | "engines": { 720 | "node": ">= 0.4.0" 721 | } 722 | }, 723 | "node_modules/vary": { 724 | "version": "1.1.2", 725 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 726 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 727 | "engines": { 728 | "node": ">= 0.8" 729 | } 730 | }, 731 | "node_modules/ws": { 732 | "version": "6.2.2", 733 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", 734 | "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", 735 | "dependencies": { 736 | "async-limiter": "~1.0.0" 737 | } 738 | } 739 | } 740 | } 741 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openmct-tutorials", 3 | "version": "0.0.1", 4 | "description": "Tutorials for Open MCT", 5 | "main": "example-server/server.js", 6 | "scripts": { 7 | "start": "node example-server/server.js", 8 | "clean": "rm -rf node_modules package-lock.json" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/nasa/openmct-tutorial.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/nasa/openmct-tutorial/issues" 18 | }, 19 | "homepage": "https://github.com/nasa/openmct-tutorial#readme", 20 | "dependencies": { 21 | "express": "^4.19.2", 22 | "express-ws": "^4.0.0", 23 | "openmct": "latest", 24 | "ws": "^6.1.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /realtime-telemetry-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic Realtime telemetry plugin using websockets. 3 | */ 4 | function RealtimeTelemetryPlugin() { 5 | return function (openmct) { 6 | var socket = new WebSocket(location.origin.replace(/^http/, 'ws') + '/realtime/'); 7 | var listener = {}; 8 | 9 | socket.onmessage = function (event) { 10 | point = JSON.parse(event.data); 11 | if (listener[point.id]) { 12 | listener[point.id](point); 13 | } 14 | }; 15 | 16 | var provider = { 17 | supportsSubscribe: function (domainObject) { 18 | return domainObject.type === 'example.telemetry'; 19 | }, 20 | subscribe: function (domainObject, callback) { 21 | listener[domainObject.identifier.key] = callback; 22 | socket.send('subscribe ' + domainObject.identifier.key); 23 | return function unsubscribe() { 24 | delete listener[domainObject.identifier.key]; 25 | socket.send('unsubscribe ' + domainObject.identifier.key); 26 | }; 27 | } 28 | }; 29 | 30 | openmct.telemetry.addProvider(provider); 31 | } 32 | } 33 | --------------------------------------------------------------------------------