├── .npmignore ├── .npmrc ├── src ├── limits-config.json ├── providers │ ├── fault-mgmt-providers │ │ ├── fault-mgmt-constants.js │ │ ├── historical-fault-provider.js │ │ ├── yamcs-fault-provider.js │ │ ├── realtime-fault-provider.js │ │ ├── fault-action-provider.js │ │ └── utils.js │ ├── user │ │ ├── operator-status-parameter.js │ │ ├── poll-question-parameter.js │ │ ├── createYamcsUser.js │ │ ├── poll-question-telemetry.js │ │ └── operator-status-telemetry.js │ ├── event-limit-provider.js │ ├── staleness-provider.js │ ├── mission-status │ │ ├── mission-status-parameter.js │ │ ├── README.md │ │ └── mission-status-telemetry.js │ ├── messages.js │ ├── limit-provider.js │ ├── latest-telemetry-provider.js │ ├── events.js │ └── commands.js ├── actions │ └── exportToCSV │ │ ├── plugin.js │ │ └── ExportToCSVAction.js ├── plugins │ └── binaryToHexFormatter │ │ ├── plugin.js │ │ ├── base64ToHex.js │ │ ├── hexToBase64.js │ │ └── BinaryToHexFormatter.js └── const.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── maintenance-type.md │ ├── enhancement-request.md │ └── bug_report.md ├── dependabot.yml ├── workflows │ ├── node.js.yml │ └── yamcs-quickstart-e2e.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── tests ├── README.md ├── git-opensource-tests.sh └── e2e │ ├── yamcs │ ├── staleness.e2e.mjs │ ├── namesToParametersMap.json │ ├── search.e2e.spec.mjs │ ├── barGraph.e2e.spec.mjs │ ├── quickstartTools.mjs │ ├── load.e2e.spec.mjs │ ├── quickstartTools.e2e.spec.mjs │ ├── filters.e2e.spec.mjs │ ├── quickstartSmoke.e2e.spec.mjs │ ├── timeline.e2e.spec.mjs │ ├── telemetryTables.e2e.spec.mjs │ ├── network.e2e.spec.mjs │ └── historicalData.e2e.spec.mjs │ ├── playwright-quickstart.config.js │ └── yamcsAppActions.mjs ├── .webpack ├── webpack.prod.mjs ├── webpack.coverage.mjs ├── webpack.common.mjs └── webpack.dev.mjs ├── example ├── index.html ├── make-example-events.mjs └── index.js ├── check-optional-dependencies.mjs ├── Makefile ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | loglevel=warn 2 | 3 | #Prevent folks from ignoring an important error when building from source 4 | engine-strict=true 5 | -------------------------------------------------------------------------------- /src/limits-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "WATCH": "cyan", 3 | "WARNING": "yellow", 4 | "DISTRESS": "orange", 5 | "CRITICAL": "red", 6 | "SEVERE": "purple" 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discussions 4 | url: https://github.com/nasa/openmct/discussions 5 | about: Have a question about the project? 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/maintenance-type.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Maintenance 3 | about: Add, update or remove documentation, tests, or dependencies. 4 | title: '' 5 | labels: type:maintenance 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Summary 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts and dependencies 2 | dist/ 3 | node_modules/ 4 | 5 | package-lock.json 6 | 7 | # e2e test artifacts 8 | tests/e2e/opensource/ 9 | tests/html-test-results/ 10 | test-results 11 | .nyc_output/ 12 | 13 | # Misc 14 | .DS_Store 15 | .vscode/settings.json 16 | 17 | quickstart/ 18 | -------------------------------------------------------------------------------- /src/providers/fault-mgmt-providers/fault-mgmt-constants.js: -------------------------------------------------------------------------------- 1 | export const FAULT_MGMT_ALARMS = 'alarms'; 2 | export const FAULT_MGMT_TYPE = 'faultManagement'; 3 | export const DEFAULT_SHELVE_DURATION = 90000; 4 | export const FAULT_MGMT_ACTIONS = Object.freeze({ 5 | SHELVE: 'shelve', 6 | UNSHELVE: 'unshelve', 7 | ACKNOWLEDGE: 'acknowledge', 8 | CLEAR: 'clear' 9 | }); 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement request 3 | about: Suggest an enhancement or new improvement for this project 4 | title: '' 5 | labels: type:enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | updates: 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | open-pull-requests-limit: 4 9 | labels: 10 | - "type:maintenance" 11 | - "dependencies" 12 | ignore: 13 | - dependency-name: "@playwright/test" #We have to source the playwright container which is not detected by Dependabot 14 | - dependency-name: "playwright-core" #We have to source the playwright container which is not detected by Dependabot 15 | - dependency-name: "@babel/eslint-parser" #Lots of noise in these type patch releases. 16 | update-types: ["version-update:semver-patch"] 17 | - dependency-name: "eslint-plugin-vue" #Lots of noise in these type patch releases. 18 | update-types: ["version-update:semver-patch"] 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "daily" 24 | labels: 25 | - "type:maintenance" 26 | - "dependencies" 27 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # e2e Testing 2 | 3 | This project is using Open MCT's e2e-as-a-dependency model. To learn more, please see the official documentation on the [Official README](https://github.com/nasa/openmct/blob/master/e2e/README.md) 4 | 5 | ## How to Run Locally 6 | 7 | ** Note: if you are running the tests to match a specific branch of openmct, you will need to run `npm run build:example:currentbranch` ** 8 | 9 | ### Makefile 10 | 11 | 1. `make test-all` 12 | 13 | ### Manually 14 | 15 | To run the tests, we recommend the following workflow which bridges two separate github repos: 16 | yamcs/quickstart and openmct-yamcs (this one). 17 | 18 | 1. `git clone yamcs/quickstart` 19 | 2. `cd docker` in yamcs/quickstart 20 | 3. `make all` in yamcs/quickstart 21 | 4. `cd openmct-yamcs` to move out of yamcs/quickstart 22 | 5. `npm install` in openmct-yamcs 23 | 6. Sanity test that yamcs is up with `npm run wait-for-yamcs` in openmct-yamcs 24 | 7. `npm run test:getopensource` 25 | 8. `npm run build:example` or `npm run build:example:master` or `npm run build:example:currentbranch` 26 | 9. `npm run test:e2e:watch` -------------------------------------------------------------------------------- /src/providers/fault-mgmt-providers/historical-fault-provider.js: -------------------------------------------------------------------------------- 1 | import { FAULT_MGMT_ALARMS, FAULT_MGMT_TYPE } from './fault-mgmt-constants.js'; 2 | import { convertDataToFaultModel } from './utils.js'; 3 | 4 | export default class HistoricalFaultProvider { 5 | constructor(url, instance, processor) { 6 | this.url = url; 7 | this.instance = instance; 8 | this.processor = processor; 9 | } 10 | 11 | /** 12 | * @param {import('openmct').DomainObject} domainObject 13 | * @returns {boolean} 14 | */ 15 | supportsRequest(domainObject) { 16 | return domainObject.type === FAULT_MGMT_TYPE; 17 | } 18 | 19 | /** 20 | * @returns {Promise} 21 | */ 22 | async request() { 23 | const url = `${this.url}api/processors/${this.instance}/${this.processor}/${FAULT_MGMT_ALARMS}`; 24 | 25 | const res = await fetch(url); 26 | const faultsData = await res.json(); 27 | 28 | return faultsData.alarms?.map(convertDataToFaultModel); 29 | } 30 | } 31 | 32 | /** 33 | * @typedef {import('./utils.js').FaultModel} FaultModel 34 | */ 35 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: npm install, build, and lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20.x, 18.x] 15 | openmct-version: 16 | - latest 17 | - stable 18 | timeout-minutes: 10 19 | continue-on-error: true 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - name: Run build:example based on openmct-version 28 | run: | 29 | if [ "${{ matrix.openmct-version }}" = "latest" ]; then 30 | npm run build:example:master 31 | elif [ "${{ matrix.openmct-version }}" = "stable" ]; then 32 | npm run build:example 33 | fi 34 | 35 | lint: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Use Node.js 20.x 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: 20.x 43 | - run: npm install 44 | - name: Run lint 45 | run: npm run lint 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: File a Bug ! 4 | title: '' 5 | labels: type:bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | #### Summary 14 | 15 | 16 | 17 | #### Expected vs Current Behavior 18 | 19 | 20 | #### Impact Check List 21 | 22 | 23 | - [ ] Data loss or misrepresented data? 24 | - [ ] Regression? Did this used to work or has it always been broken? 25 | - [ ] Is there a workaround available? 26 | - [ ] Does this impact a critical component? 27 | - [ ] Is this just a visual bug with no functional impact? 28 | 29 | #### Steps to Reproduce 30 | 31 | 32 | 1. 33 | 2. 34 | 3. 35 | 4. 36 | 37 | #### Environment 38 | * Open MCT Version: 39 | * Deployment Type: 40 | * OS: 41 | * Browser: 42 | 43 | #### Additional Information 44 | 45 | -------------------------------------------------------------------------------- /src/actions/exportToCSV/plugin.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2023, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | import ExportToCSVAction from './ExportToCSVAction.js'; 23 | 24 | export default function (url, instance) { 25 | return function (openmct) { 26 | openmct.actions.register(new ExportToCSVAction(openmct, url, instance)); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/plugins/binaryToHexFormatter/plugin.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import BinaryToHexFormatter from './BinaryToHexFormatter.js'; 24 | 25 | export default function BinaryToHexFormatterPlugin() { 26 | return function (openmct) { 27 | openmct.telemetry.addFormat(new BinaryToHexFormatter()); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /.webpack/webpack.prod.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2020, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import { merge } from 'webpack-merge'; 24 | import common from './webpack.common.mjs'; 25 | 26 | /** @type {import('webpack').Configuration} */ 27 | const prodConfig = { 28 | mode: 'production', 29 | devtool: 'source-map' 30 | } 31 | export default merge(common, prodConfig); 32 | -------------------------------------------------------------------------------- /src/plugins/binaryToHexFormatter/base64ToHex.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Converts a base64-encoded string to its hexadecimal representation. This function first decodes 3 | * the base64 string to a raw binary string using the atob() function. Then, it iterates over each 4 | * character in the binary string, converting it to its corresponding hexadecimal value. Each byte 5 | * (character) is converted to a two-digit hexadecimal number, and these hexadecimal numbers are 6 | * concatenated to form the final hexadecimal string representation of the input base64 data. 7 | * 8 | * Parameters: 9 | * base64 the base64-encoded string to be converted 10 | * 11 | * Returns: 12 | * A string representing the hexadecimal representation of the decoded base64 input. Each byte 13 | * of the input data is represented as two hexadecimal digits in the output string. 14 | * 15 | * Example Usage: 16 | * const base64String = 'SGVsbG8gV29ybGQ='; // "Hello World" in base64 17 | * const hexString = base64ToHex(base64String); 18 | * console.log(hexString); // Outputs the hexadecimal representation of "Hello World" 19 | * 20 | * Note: 21 | * This function assumes a browser environment where atob() is available for base64 decoding. 22 | */ 23 | 24 | export default function base64ToHex(base64) { 25 | const binaryString = atob(base64); 26 | 27 | let hexString = ''; 28 | for (let i = 0; i < binaryString.length; i++) { 29 | const charCode = binaryString.charCodeAt(i); 30 | 31 | hexString += charCode.toString(16).padStart(2, '0'); 32 | } 33 | 34 | return `0x${hexString.toUpperCase()}`; 35 | } 36 | -------------------------------------------------------------------------------- /tests/git-opensource-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This script can clone/checkout a single folder from git repository 4 | # - Might be used for checking out micro-services from monolithic git repository 5 | # 6 | # - You can even do checkout into home directory, for example 7 | # git-sparse-clone.sh git@github.com:readdle/fluix-web.git /home/login login 8 | # 9 | 10 | SCRIPT_PATH=${0%/*} # Get the relative path to the script dir from the cwd 11 | if [ "$0" != "$SCRIPT_PATH" ] && [ "$SCRIPT_PATH" != "" ]; then 12 | cd "$SCRIPT_PATH" 13 | fi 14 | 15 | rm -rf e2e/opensource 16 | 17 | # This will cause the shell to exit immediately if a simple command exits with a nonzero exit value. 18 | set -e 19 | 20 | REPO_URL=https://github.com/nasa/openmct.git 21 | REPO_PATH=e2e 22 | LOCAL_REPO_ROOT="e2e/opensource" 23 | 24 | # remove the branch later 25 | git clone --no-checkout --depth 1 $REPO_URL "$LOCAL_REPO_ROOT" 26 | cd "$LOCAL_REPO_ROOT" 27 | git config core.sparsecheckout true 28 | echo "/$REPO_PATH/**" > .git/info/sparse-checkout 29 | git read-tree -m -u HEAD 30 | 31 | # moving back to /tests/ dir 32 | cd .. 33 | 34 | # Move index.js to root 35 | mv opensource/e2e/index.js ./opensource 36 | # Move package.json, package-lock.json 37 | mv opensource/e2e/package*.json ./opensource 38 | # Move fixtures and appActions 39 | mv opensource/e2e/*Fixtures.js ./opensource 40 | mv opensource/e2e/appActions.js ./opensource 41 | mv opensource/e2e/constants.js ./opensource 42 | # Move subfolders 43 | mv opensource/e2e/*/ ./opensource 44 | # Move eslint config 45 | mv opensource/e2e/.eslintrc.*js ./opensource 46 | # Cleanup 47 | rm -rf opensource/e2e 48 | -------------------------------------------------------------------------------- /src/providers/fault-mgmt-providers/yamcs-fault-provider.js: -------------------------------------------------------------------------------- 1 | import HistoricalFaultProvider from './historical-fault-provider.js'; 2 | import RealtimeFaultProvider from './realtime-fault-provider.js'; 3 | import FaultActionProvider from './fault-action-provider.js'; 4 | 5 | const DEFAULT_PROCESSOR = 'realtime'; 6 | 7 | export default class YamcsFaultProvider { 8 | constructor(openmct, { historicalEndpoint, yamcsInstance, yamcsProcessor = DEFAULT_PROCESSOR } = {}) { 9 | this.historicalFaultProvider = new HistoricalFaultProvider( 10 | historicalEndpoint, 11 | yamcsInstance, 12 | yamcsProcessor 13 | ); 14 | 15 | this.realtimeFaultProvider = new RealtimeFaultProvider( 16 | openmct, 17 | yamcsInstance 18 | ); 19 | 20 | this.faultActionProvider = new FaultActionProvider( 21 | historicalEndpoint, 22 | yamcsInstance, 23 | yamcsProcessor 24 | ); 25 | 26 | this.request = this.historicalFaultProvider.request.bind(this.historicalFaultProvider); 27 | this.subscribe = this.realtimeFaultProvider.subscribe.bind(this.realtimeFaultProvider); 28 | this.supportsRequest = this.historicalFaultProvider.supportsRequest.bind(this.historicalFaultProvider); 29 | this.supportsSubscribe = this.realtimeFaultProvider.supportsSubscribe.bind(this.realtimeFaultProvider); 30 | this.acknowledgeFault = this.faultActionProvider.acknowledgeFault.bind(this.faultActionProvider); 31 | this.shelveFault = this.faultActionProvider.shelveFault.bind(this.faultActionProvider); 32 | this.getShelveDurations = this.faultActionProvider.getShelveDurations.bind(this.faultActionProvider); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | Open MCT - YAMCS Example 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | Closes 3 | 4 | ### Describe your changes: 5 | 6 | 7 | ### All Submissions: 8 | 9 | * [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)? 10 | * [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change? 11 | * [ ] Is this a [notable change](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins? 12 | 13 | ### Author Checklist 14 | 15 | * [ ] Changes address original issue? 16 | * [ ] Tests included and/or updated with changes? 17 | * [ ] Has this been smoke tested? 18 | * [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue. 19 | * [ ] Have you associated a milestone with this PR? Note: leave blank if unsure. 20 | * [ ] Testing instructions included in associated issue OR is this a dependency/testcase change? 21 | 22 | ### Reviewer Checklist 23 | 24 | * [ ] Changes appear to address issue? 25 | * [ ] Reviewer has tested changes by following the provided instructions? 26 | * [ ] Changes appear not to be breaking changes? 27 | * [ ] Appropriate automated tests included? 28 | * [ ] Code style and in-line documentation are appropriate? 29 | -------------------------------------------------------------------------------- /.webpack/webpack.coverage.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2020, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import config from './webpack.dev.mjs'; 24 | 25 | config.devtool = 'source-map'; 26 | 27 | config.devServer.hot = false; 28 | 29 | config.module.rules.push({ 30 | test: /\.(mjs|js)$/, 31 | exclude: /(node_modules)/, 32 | use: { 33 | loader: 'babel-loader', 34 | options: { 35 | retainLines: true, 36 | plugins: [['babel-plugin-istanbul', { 37 | extension: ['.js'] 38 | }]] 39 | } 40 | } 41 | }); 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /src/plugins/binaryToHexFormatter/hexToBase64.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Converts a hexadecimal string into its base64 representation. This function first strips any '0x' prefix 3 | * from the input hexadecimal string to ensure proper conversion. It then iterates over the hexadecimal string 4 | * in 2-character increments, each representing a byte, converting these hexadecimal pairs into binary data. 5 | * This binary data is subsequently encoded into a base64 string using the btoa() function, which is a built-in 6 | * browser API for base64 encoding of binary strings. 7 | * 8 | * Parameters: 9 | * hexStr The hexadecimal string to be converted to base64. Can optionally start with '0x'. 10 | * 11 | * Returns: 12 | * A string representing the base64 encoded form of the input hexadecimal data. 13 | * 14 | * Example Usage: 15 | * const hexString = '48656C6C6F'; // Hexadecimal for 'Hello' 16 | * const base64String = hexToBase64(hexString); 17 | * console.log(base64String); // Outputs the base64 representation of 'Hello' 18 | * 19 | * Note: 20 | * This function is intended to be used in environments where the btoa() function is available, 21 | * such as web browsers. For Node.js or other environments, an alternative base64 encoding method 22 | * may be required. 23 | * 24 | * It's important to ensure that the input hexadecimal string is a valid representation of binary data, 25 | * with even length and only containing valid hexadecimal digits (0-9, A-F, a-f). 26 | */ 27 | 28 | export default function hexToBase64(hexStr) { 29 | // just in case 30 | hexStr = hexStr.replace(/^0x/, ''); 31 | 32 | let binaryStr = ''; 33 | 34 | for (let i = 0; i < hexStr.length; i += 2) { 35 | binaryStr += String.fromCharCode(parseInt(hexStr.substr(i, 2), 16)); 36 | } 37 | 38 | return btoa(binaryStr); 39 | } 40 | -------------------------------------------------------------------------------- /src/providers/user/operator-status-parameter.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | const OPERATOR_STATUS_TYPE = 'yamcs.operatorStatus'; 24 | 25 | export function isOperatorStatusParameter(parameter) { 26 | const aliases = parameter.alias; 27 | 28 | return aliases !== undefined 29 | && aliases.some(alias => alias.name === OPERATOR_STATUS_TYPE); 30 | } 31 | 32 | export function getRoleFromParameter(parameter) { 33 | const aliases = parameter.alias; 34 | 35 | return aliases.find(alias => alias.namespace === 'OpenMCT:role')?.name; 36 | } 37 | 38 | export function getPossibleStatusesFromParameter(parameter) { 39 | return parameter.type.enumValue; 40 | } 41 | -------------------------------------------------------------------------------- /src/plugins/binaryToHexFormatter/BinaryToHexFormatter.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import base64ToHex from './base64ToHex.js'; 24 | import hexToBase64 from './hexToBase64.js'; 25 | 26 | export default class BinaryToHexFormatter { 27 | constructor() { 28 | this.key = 'binary'; 29 | } 30 | 31 | format(value) { 32 | if (value === undefined) { 33 | return value; 34 | } 35 | 36 | return base64ToHex(value); 37 | } 38 | 39 | parse(text) { 40 | if (text === undefined) { 41 | return text; 42 | } 43 | 44 | return hexToBase64(text); 45 | } 46 | 47 | validate(text) { 48 | try { 49 | atob(text); 50 | } catch (e) { 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/providers/fault-mgmt-providers/realtime-fault-provider.js: -------------------------------------------------------------------------------- 1 | import { FAULT_MGMT_TYPE } from './fault-mgmt-constants.js'; 2 | import { DATA_TYPES, NAMESPACE, OBJECT_TYPES } from '../../const.js'; 3 | import { convertDataToFaultModel } from './utils.js'; 4 | 5 | export default class RealtimeFaultProvider { 6 | #openmct; 7 | constructor(openmct, instance) { 8 | this.#openmct = openmct; 9 | this.instance = instance; 10 | 11 | this.lastSubscriptionId = 1; 12 | this.subscriptionsByCall = new Map(); 13 | this.subscriptionsById = {}; 14 | 15 | this.ALARMS_OBJECT = Object.freeze({ 16 | identifier: { 17 | namespace: NAMESPACE, 18 | key: OBJECT_TYPES.ALARMS_TYPE 19 | }, 20 | type: OBJECT_TYPES.ALARMS_TYPE 21 | }); 22 | 23 | this.GLOBAL_STATUS_OBJECT = Object.freeze({ 24 | identifier: { 25 | namespace: NAMESPACE, 26 | key: OBJECT_TYPES.GLOBAL_STATUS_TYPE 27 | }, 28 | type: OBJECT_TYPES.GLOBAL_STATUS_TYPE 29 | }); 30 | } 31 | 32 | supportsSubscribe(domainObject) { 33 | return domainObject.type === FAULT_MGMT_TYPE; 34 | } 35 | 36 | subscribe(domainObject, callback) { 37 | const globalUnsubscribe = this.#openmct.telemetry.subscribe( 38 | this.GLOBAL_STATUS_OBJECT, 39 | (response) => { 40 | this.handleResponse(DATA_TYPES.DATA_TYPE_GLOBAL_STATUS, response, callback); 41 | }); 42 | 43 | const alarmsUnsubscribe = this.#openmct.telemetry.subscribe( 44 | this.ALARMS_OBJECT, 45 | (response) => { 46 | this.handleResponse(DATA_TYPES.DATA_TYPE_ALARMS, response, callback); 47 | }); 48 | 49 | return () => { 50 | globalUnsubscribe(); 51 | alarmsUnsubscribe(); 52 | }; 53 | } 54 | 55 | handleResponse(type, response, callback) { 56 | callback(convertDataToFaultModel(response, type)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/staleness.e2e.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | Staleness Specific Tests 25 | */ 26 | 27 | import { pluginFixtures } from 'openmct-e2e'; 28 | const { test } = pluginFixtures; 29 | 30 | test.describe.fixme("Staleness tests @yamcs", () => { 31 | // eslint-disable-next-line require-await 32 | test('Staleness ', async ({ page }) => { 33 | test.step('Indicator is displayed for historic data', () => { 34 | // Create a plot 35 | // Add a telemetry endpoint that has stale data to this plot 36 | // Expect that there is indication of staleness for the plot 37 | }); 38 | 39 | test.step('Indicator is removed when new data arrives in real time', () => { 40 | // Wait for new data 41 | // Expect that stale indication is removed 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/namesToParametersMap.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": "/myproject/A", 3 | "ADCS_Error_Flag": "/myproject/ADCS_Error_Flag", 4 | "Battery1_Temp": "/myproject/Battery1_Temp", 5 | "Battery1_Voltage": "/myproject/Battery1_Voltage", 6 | "Battery2_Temp": "/myproject/Battery2_Temp", 7 | "Battery2_Voltage": "/myproject/Battery2_Voltage", 8 | "CCSDS_Packet_Length": "/myproject/CCSDS_Packet_Length", 9 | "CDHS_Error_Flag": "/myproject/CDHS_Error_Flag", 10 | "CDHS_Status": "/myproject/CDHS_Status", 11 | "COMMS_Error_Flag": "/myproject/COMMS_Error_Flag", 12 | "COMMS_Status": "/myproject/COMMS_Status", 13 | "Contact_Golbasi_GS": "/myproject/Contact_Golbasi_GS", 14 | "Contact_Svalbard": "/myproject/Contact_Svalbard", 15 | "Detector_Temp": "/myproject/Detector_Temp", 16 | "ElapsedSeconds": "/myproject/ElapsedSeconds", 17 | "Enum_Para_1": "/myproject/Enum_Para_1", 18 | "Enum_Para_2": "/myproject/Enum_Para_2", 19 | "Enum_Para_3": "/myproject/Enum_Para_3", 20 | "EpochUSNO": "/myproject/EpochUSNO", 21 | "EPS_Error_Flag": "/myproject/EPS_Error_Flag", 22 | "Gyro.x": "/myproject/Gyro.x", 23 | "Gyro.y": "/myproject/Gyro.y", 24 | "Gyro.z": "/myproject/Gyro.z", 25 | "Height": "/myproject/Height", 26 | "Latitude": "/myproject/Latitude", 27 | "Longitude": "/myproject/Longitude", 28 | "Magnetometer.x": "/myproject/Magnetometer.x", 29 | "Magnetometer.y": "/myproject/Magnetometer.y", 30 | "Magnetometer.z": "/myproject/Magnetometer.z", 31 | "Mode_Day": "/myproject/Mode_Day", 32 | "Mode_Night": "/myproject/Mode_Night", 33 | "Mode_Payload": "/myproject/Mode_Payload", 34 | "Mode_Safe": "/myproject/Mode_Safe", 35 | "Mode_SBand": "/myproject/Mode_SBand", 36 | "Mode_XBand": "/myproject/Mode_XBand", 37 | "OrbitNumberCumulative": "/myproject/OrbitNumberCumulative", 38 | "Payload_Error_Flag": "/myproject/Payload_Error_Flag", 39 | "Payload_Status": "/myproject/Payload_Status", 40 | "Position.x": "/myproject/Position.x", 41 | "Position.y": "/myproject/Position.y", 42 | "Position.z": "/myproject/Position.z", 43 | "Shadow": "/myproject/Shadow", 44 | "Sunsensor": "/myproject/Sunsensor", 45 | "Velocity.x": "/myproject/Velocity.x", 46 | "Velocity.y": "/myproject/Velocity.y", 47 | "Velocity.z": "/myproject/Velocity.z" 48 | } -------------------------------------------------------------------------------- /.webpack/webpack.common.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2020, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import path from 'node:path'; 24 | import { fileURLToPath } from 'node:url'; 25 | 26 | const projectRootDir = fileURLToPath(new URL('../', import.meta.url)); 27 | 28 | /** @type {import('webpack').Configuration} */ 29 | const commonConfig = { 30 | context: projectRootDir, 31 | performance: { 32 | hints: false 33 | }, 34 | entry: { 35 | 'openmct-yamcs': './src/openmct-yamcs.js' 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.js$/, 41 | enforce: "pre", 42 | use: ["source-map-loader"] 43 | } 44 | ], 45 | }, 46 | output: { 47 | globalObject: "this", 48 | filename: '[name].js', 49 | path: path.resolve(projectRootDir, 'dist'), 50 | library: { 51 | type: 'umd', 52 | export: 'default', 53 | name: 'openmctYamcs' 54 | } 55 | } 56 | }; 57 | 58 | export default commonConfig; 59 | 60 | -------------------------------------------------------------------------------- /src/providers/event-limit-provider.js: -------------------------------------------------------------------------------- 1 | /* CSS classes for Yamcs parameter monitoring result values. */ 2 | 3 | import { OBJECT_TYPES } from "../const"; 4 | 5 | const SEVERITY_CSS = { 6 | 'WATCH': 'is-event--yellow', 7 | 'WARNING': 'is-event--yellow', 8 | 'DISTRESS': 'is-event--red', 9 | 'CRITICAL': 'is-event--red', 10 | 'SEVERE': 'is-event--red' 11 | }; 12 | 13 | const NOMINAL_SEVERITY = { 14 | cssClass: 'is-event--no-style', 15 | name: 'NOMINAL' 16 | }; 17 | 18 | /** 19 | * @typedef {Object} EvaluationResult 20 | * @property {string} cssClass CSS class information 21 | * @property {string} name a severity name 22 | */ 23 | export default class EventLimitProvider { 24 | constructor(openmct) { 25 | this.openmct = openmct; 26 | } 27 | 28 | getLimitEvaluator(domainObject) { 29 | const self = this; 30 | 31 | return { 32 | /** 33 | * Evaluates a telemetry datum for severity. 34 | * 35 | * @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum}) 36 | * @param {object} valueMetadata metadata about the telemetry datum 37 | * 38 | * @returns {EvaluationResult} ({@link EvaluationResult}) 39 | */ 40 | evaluate: function (datum, valueMetadata) { 41 | // prevent applying the class to the tr, only to td 42 | if (!valueMetadata) { 43 | return; 44 | } 45 | 46 | if (datum.severity in SEVERITY_CSS) { 47 | return self.getSeverity(datum, valueMetadata); 48 | } 49 | 50 | return NOMINAL_SEVERITY; 51 | } 52 | 53 | }; 54 | } 55 | getSeverity(datum, valueMetadata) { 56 | if (!valueMetadata) { 57 | return; 58 | } 59 | 60 | const severityValue = datum.severity; 61 | 62 | return { 63 | cssClass: SEVERITY_CSS[severityValue], 64 | name: severityValue 65 | }; 66 | 67 | } 68 | 69 | supportsLimits(domainObject) { 70 | return [OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE].includes(domainObject.type); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/providers/user/poll-question-parameter.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | const POLL_QUESTION_TYPE = 'yamcs.pollQuestion'; 24 | 25 | export default class PollQuestionParameter { 26 | #pollQuestionParameterResolve; 27 | #pollQuestionParameterPromise; 28 | #pollQuestionParameter; 29 | 30 | constructor() { 31 | this.#pollQuestionParameterPromise = new Promise((resolve) => { 32 | this.#pollQuestionParameterResolve = resolve; 33 | }); 34 | } 35 | 36 | async isPollQuestionParameterName(parameterName) { 37 | await this.#pollQuestionParameterPromise; 38 | 39 | return this.#pollQuestionParameter.qualifiedName === parameterName; 40 | } 41 | 42 | isPollQuestionParameter(parameter) { 43 | const aliases = parameter.alias; 44 | 45 | return aliases !== undefined 46 | && aliases.some(alias => alias.name === POLL_QUESTION_TYPE); 47 | } 48 | 49 | setPollQuestionParameter(parameter) { 50 | this.#pollQuestionParameter = parameter; 51 | this.#pollQuestionParameterResolve(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/providers/staleness-provider.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2023, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import { OBJECT_TYPES, STALENESS_STATUS_MAP } from '../const.js'; 24 | import { buildStalenessResponseObject } from '../utils.js'; 25 | 26 | export default class YamcsStalenessProvider { 27 | constructor(realtimeTelemetryProvider, latestTelemetryProvider) { 28 | this.realtimeTelemetryProvider = realtimeTelemetryProvider; 29 | this.latestTelemetryProvider = latestTelemetryProvider; 30 | } 31 | 32 | supportsStaleness(domainObject) { 33 | return domainObject.type === OBJECT_TYPES.TELEMETRY_OBJECT_TYPE; 34 | } 35 | 36 | subscribeToStaleness(domainObject, callback) { 37 | return this.realtimeTelemetryProvider.subscribeToStaleness(domainObject, callback); 38 | } 39 | 40 | async isStale(domainObject) { 41 | const response = await this.latestTelemetryProvider.requestLatest(domainObject); 42 | 43 | if (!response?.acquisitionStatus) { 44 | return; 45 | } 46 | 47 | const stalenessObject = buildStalenessResponseObject( 48 | STALENESS_STATUS_MAP[response.acquisitionStatus], 49 | response.timestamp 50 | ); 51 | 52 | return stalenessObject; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /check-optional-dependencies.mjs: -------------------------------------------------------------------------------- 1 | // check-optional-dependencies.mjs 2 | import fs from 'node:fs/promises'; 3 | import semver from 'semver'; 4 | import process from 'node:process'; 5 | 6 | /** 7 | * Checks if an optional dependency satisfies the expected version. 8 | * 9 | * @param {string} dependency - The name of the dependency to check. 10 | * @param {string} expectedVersion - The semver range the version should satisfy. 11 | * @returns {Promise} A promise that resolves with an error message if the dependency does not satisfy the expected version, or is not installed; otherwise, null. 12 | */ 13 | async function checkOptionalDependency(dependency, expectedVersion) { 14 | try { 15 | const packageJsonPath = new URL(`./node_modules/${dependency}/package.json`, import.meta.url).pathname; 16 | const installedPackageJsonData = await fs.readFile(packageJsonPath, { encoding: 'utf8' }); 17 | const { version: installedVersion } = JSON.parse(installedPackageJsonData); 18 | 19 | if (!semver.satisfies(installedVersion, expectedVersion)) { 20 | return `The installed version of optional dependency ${dependency} is ${installedVersion}, which does not satisfy the expected version ${expectedVersion}. Please update it before building.`; 21 | } 22 | } catch (error) { 23 | return `The optional dependency ${dependency} is not installed. Please install it before building.\n${error}`; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | /** 30 | * Checks all optional dependencies listed in the package.json against their expected versions. 31 | * 32 | * @returns {Promise} A promise that resolves if all optional dependencies satisfy their expected versions, or rejects with an error message if any do not. 33 | */ 34 | async function checkAllOptionalDependencies() { 35 | const packageJsonPath = new URL('./package.json', import.meta.url); 36 | const myPackageJsonData = await fs.readFile(packageJsonPath); 37 | const { optionalDependencies } = JSON.parse(myPackageJsonData); 38 | 39 | const checks = Object.entries(optionalDependencies).map(([dependency, expectedVersion]) => 40 | checkOptionalDependency(dependency, expectedVersion) 41 | ); 42 | 43 | const results = await Promise.all(checks); 44 | const errors = results.filter(result => result !== null); 45 | 46 | if (errors.length > 0) { 47 | console.error(errors.join('\n')); 48 | process.exit(1); 49 | } 50 | } 51 | 52 | checkAllOptionalDependencies().catch(error => { 53 | console.error(error); 54 | process.exit(1); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/e2e/playwright-quickstart.config.js: -------------------------------------------------------------------------------- 1 | // playwright.config.js 2 | // @ts-check 3 | 4 | /** @type {import('@playwright/test').PlaywrightTestConfig<{ failOnConsoleError: boolean, myItemsFolderName: string }>} */ 5 | const config = { 6 | retries: 1, 7 | testDir: '.', 8 | testMatch: /.*\.e2e\.spec\.(mjs|js)$/, 9 | timeout: 30 * 1000, 10 | use: { 11 | headless: false, 12 | video: 'off', 13 | screenshot: 'on', 14 | trace: 'on', 15 | baseURL: 'http://localhost:9000/#', 16 | ignoreHTTPSErrors: true, 17 | myItemsFolderName: "My Items", 18 | failOnConsoleError: false, 19 | storageState: { 20 | cookies: [], 21 | origins: [ 22 | { 23 | "origin": "http://localhost:9000", 24 | "localStorage": [ 25 | { 26 | "name": "exampleLayout", 27 | "value": "false" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | }, 34 | webServer: { 35 | cwd: '../', 36 | command: 'npm run start:coverage', 37 | url: 'http://localhost:9000/#', 38 | timeout: 120 * 1000, 39 | reuseExistingServer: false 40 | }, 41 | workers: 1, 42 | projects: [ 43 | { 44 | name: "chromium", 45 | grepInvert: /@unstable|@snapshot|@localStorage|@addInit/, 46 | use: { 47 | browserName: 'chromium', 48 | headless: true, 49 | trace: 'on-first-retry', 50 | video: 'off', 51 | screenshot: 'only-on-failure' 52 | } 53 | }, 54 | // -- Local Browsers -- 55 | { 56 | name: "local-chrome", 57 | grepInvert: /@unstable|@snapshot|@localStorage|@addInit/, 58 | use: { 59 | browserName: 'chromium', 60 | channel: 'chrome' 61 | } 62 | }, 63 | { 64 | name: "local-webkit", 65 | use: { 66 | browserName: 'webkit' 67 | } 68 | }, 69 | { 70 | name: "local-firefox", 71 | use: { 72 | browserName: 'firefox' 73 | } 74 | } 75 | ], 76 | reporter: [ 77 | ['list'], 78 | ['html', { 79 | open: 'never', 80 | outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 81 | }], 82 | ['junit', { outputFile: 'test-results/results.xml' }]] 83 | }; 84 | 85 | export default config; 86 | 87 | -------------------------------------------------------------------------------- /.webpack/webpack.dev.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2020, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | // @ts-check 23 | import path from "path"; 24 | import { fileURLToPath } from "url"; 25 | import { merge } from "webpack-merge"; 26 | import commonConfig from "./webpack.common.mjs"; 27 | 28 | // Replicate __dirname functionality for ES modules 29 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 30 | 31 | /** @type {import('webpack').Configuration} */ 32 | const devConfig = { 33 | mode: "development", 34 | context: path.resolve(__dirname, "../"), 35 | devtool: "eval-source-map", 36 | entry: { 37 | "openmct-yamcs-example": path.resolve(__dirname, "../example/index.js"), 38 | }, 39 | devServer: { 40 | compress: true, 41 | port: 9000, 42 | static: [ 43 | { 44 | directory: path.join(__dirname, "../example"), 45 | }, 46 | { 47 | directory: path.join(__dirname, "../node_modules/openmct/dist"), 48 | publicPath: "/node_modules/openmct/dist", 49 | }, 50 | ], 51 | proxy: [ 52 | { 53 | context: ["/yamcs-proxy/"], 54 | target: "http://0.0.0.0:8090/", 55 | secure: false, 56 | changeOrigin: true, 57 | pathRewrite: { "^/yamcs-proxy/": "" }, 58 | }, 59 | { 60 | context: ["/yamcs-proxy-ws/"], 61 | target: "ws://0.0.0.0:8090/api/websocket", 62 | secure: false, 63 | changeOrigin: true, 64 | ws: true, 65 | pathRewrite: { "^/yamcs-proxy-ws/": "" }, 66 | }, 67 | ], 68 | }, 69 | }; 70 | 71 | export default merge(commonConfig, devConfig); 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clone-quickstart install-quickstart start-quickstart install-openmct-yamcs sanity-test build-example test-getopensource test-e2e clean 2 | 3 | test-all: clone-quickstart install-quickstart install-openmct-yamcs sanity-test build-example test-e2e 4 | 5 | start-all: clone-quickstart install-quickstart install-openmct-yamcs sanity-test build-example start-openmct 6 | 7 | clone-quickstart: 8 | @echo "Running target: clone-quickstart" 9 | @echo "Current working directory: $(shell pwd)" 10 | if [ ! -d "quickstart" ]; then \ 11 | git clone https://github.com/yamcs/quickstart; \ 12 | else \ 13 | echo "Directory 'quickstart' already exists."; \ 14 | fi 15 | 16 | install-quickstart: 17 | @echo "Running target: install-quickstart" 18 | @cd quickstart/docker && $(MAKE) wait-for-sent 19 | 20 | start-quickstart: 21 | @echo "Running target: start-quickstart" 22 | @cd quickstart/docker && $(MAKE) all 23 | 24 | reset-quickstart: 25 | @echo "Running target: reset-quickstart" 26 | @cd quickstart/docker && $(MAKE) yamcs-simulator-restart 27 | 28 | install-openmct-yamcs: 29 | @echo "Running target: install-openmct-yamcs" 30 | npm install 31 | 32 | sanity-test: 33 | @echo "Running target: sanity-test" 34 | npm run wait-for-yamcs 35 | 36 | build-example: #This will run build example based on the current branch of openmct-yamcs and fallback to master 37 | @echo "Running target: build-example" 38 | current_branch=$(shell git rev-parse --abbrev-ref HEAD); \ 39 | echo "Current branch of openmct-yamcs: $$current_branch checking if it exists in openmct repository"; \ 40 | if git ls-remote --exit-code --heads https://github.com/nasa/openmct.git refs/heads/$$current_branch; then \ 41 | echo "Branch $$current_branch exists in openmct repository. Running build:example:currentbranch"; \ 42 | npm run build:example:currentbranch || { echo "Failed to run build:example:currentbranch"; exit 1; }; \ 43 | else \ 44 | echo "Branch $$current_branch does not exist in openmct repository. Running build:example:master"; \ 45 | npm run build:example:master || { echo "Failed to run build:example:master"; exit 1; }; \ 46 | fi 47 | 48 | start-openmct: 49 | @echo "Running target: start-openmct" 50 | npm start 51 | 52 | test-e2e: 53 | @echo "Running target: test-e2e" 54 | npm run test:getopensource 55 | npm run test:e2e:quickstart:local 56 | 57 | clean: 58 | @echo "Running target: clean" 59 | npm run clean 60 | echo "Ran npm run clean." 61 | @if [ -d "quickstart/docker" ]; then \ 62 | echo "Directory 'quickstart/docker' exists. Running make clean in quickstart/docker."; \ 63 | cd quickstart/docker && $(MAKE) clean; \ 64 | cd ../..; \ 65 | rm -rf quickstart; \ 66 | echo "Removed 'quickstart' directory."; \ 67 | else \ 68 | echo "Directory 'quickstart/docker' does not exist. Skipping."; \ 69 | fi 70 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/search.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | Search Specific Tests 25 | */ 26 | 27 | import { pluginFixtures } from 'openmct-e2e'; 28 | const { test, expect } = pluginFixtures; 29 | 30 | test.describe("Quickstart search tests @yamcs", () => { 31 | test('Validate aggregate in search result', async ({ page }) => { 32 | // Go to baseURL 33 | await page.goto("./", { waitUntil: "networkidle" }); 34 | 35 | await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); 36 | // Search for Sequence 37 | await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Sequence'); 38 | 39 | await expect(page.getByLabel('Object Search Result').nth(0)).toContainText("CCSDS_Packet_Sequence"); 40 | await expect(page.getByLabel('Object Search Result').nth(1)).toContainText("CCSDS_Packet_Sequence.GroupFlags"); 41 | await expect(page.getByLabel('Object Search Result').nth(2)).toContainText("CCSDS_Packet_Sequence.Count"); 42 | // Search for mixed case and get same results 43 | await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('seQuence'); 44 | await expect(page.getByLabel('Object Search Result').nth(0)).toContainText("CCSDS_Packet_Sequence"); 45 | await expect(page.getByLabel('Object Search Result').nth(1)).toContainText("CCSDS_Packet_Sequence.GroupFlags"); 46 | await expect(page.getByLabel('Object Search Result').nth(2)).toContainText("CCSDS_Packet_Sequence.Count"); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/e2e/yamcsAppActions.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /** 24 | * The fixtures in this file are to be used to consolidate common actions performed by the 25 | * various test suites. The goal is only to avoid duplication of code across test suites and not to abstract 26 | * away the underlying functionality of the application. For more about the App Action pattern, see /e2e/README.md) 27 | * 28 | * For example, if two functions are nearly identical in 29 | * timer.e2e.spec.js and notebook.e2e.spec.js, that function should be generalized and moved into this file. 30 | */ 31 | 32 | /** 33 | * Search for telemetry and link it to an object. objectName should come from the domainObject.name function. 34 | * @param {import('@playwright/test').Page} page 35 | * @param {string} parameterName 36 | * @param {string} objectName 37 | */ 38 | async function searchAndLinkTelemetryToObject(page, parameterName, objectName) { 39 | await page.getByRole('searchbox', { name: 'Search Input' }).click(); 40 | await page.getByRole('searchbox', { name: 'Search Input' }).fill(parameterName); 41 | await page.getByLabel(`${parameterName} yamcs.aggregate result`).getByText(parameterName).click(); 42 | await page.getByLabel('More actions').click(); 43 | await page.getByLabel('Create Link').click(); 44 | await page.getByLabel('Modal Overlay').getByLabel('Search Input').click(); 45 | await page.getByLabel('Modal Overlay').getByLabel('Search Input').fill(objectName); 46 | await page.getByLabel('Navigate to Bar Graph').click(); 47 | await page.getByText('Ok').click(); 48 | } 49 | 50 | export { 51 | searchAndLinkTelemetryToObject 52 | }; 53 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/barGraph.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | * This test suite is dedicated to testing the Bar Graph component. 25 | */ 26 | 27 | import { pluginFixtures, appActions } from 'openmct-e2e'; 28 | import { searchAndLinkTelemetryToObject } from '../yamcsAppActions.mjs'; 29 | const { test, expect } = pluginFixtures; 30 | const { createDomainObjectWithDefaults, setFixedTimeMode } = appActions; 31 | 32 | test.describe('Bar Graph @yamcs', () => { 33 | let barGraph; 34 | let historicalGet; 35 | 36 | test.beforeEach(async ({ page }) => { 37 | // Open a browser, navigate to the main page, and wait until all networkevents to resolve 38 | await page.goto('./', { waitUntil: 'networkidle' }); 39 | // Set fixed time mode 40 | await page.evaluate(() => openmct.time.setMode('fixed', {start: Date.now() - 30000, end: Date.now()})); 41 | await page.waitForURL(/tc\.mode=fixed/); 42 | // Create the Bar Graph 43 | barGraph = await createDomainObjectWithDefaults(page, { type: 'Graph', name: 'Bar Graph' }); 44 | // Enter edit mode for the overlay plot 45 | await searchAndLinkTelemetryToObject(page, 'Magnetometer', barGraph.name); 46 | }); 47 | 48 | test('Requests a single historical datum', async ({ page }) => { 49 | 50 | //http://localhost:9000/yamcs-proxy/api/archive/myproject/parameters/myproject/Magnetometer?start=2024-09-25T14%3A08%3A46.244Z&stop=2024-09-25T14%3A38%3A46.245Z&limit=1&order=desc 51 | historicalGet = page.waitForResponse(/.*\/api\/.*\/parameters.*limit=1&order=desc$/); 52 | 53 | await page.goto(barGraph.url, { waitUntil: 'networkidle' }); 54 | 55 | await historicalGet; 56 | 57 | await expect(page.getByRole('main').getByText(barGraph.name)).toBeVisible(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/const.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2021, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | export const OBJECT_TYPES = { 24 | COMMANDS_ROOT_OBJECT_TYPE: 'yamcs.commands', 25 | COMMANDS_QUEUE_OBJECT_TYPE: 'yamcs.commands.queue', 26 | EVENTS_ROOT_OBJECT_TYPE: 'yamcs.events', 27 | EVENT_SPECIFIC_OBJECT_TYPE: 'yamcs.event.specific', 28 | EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE: 'yamcs.event.specific.severity', 29 | TELEMETRY_OBJECT_TYPE: 'yamcs.telemetry', 30 | IMAGE_OBJECT_TYPE: 'yamcs.image', 31 | STRING_OBJECT_TYPE: 'yamcs.string', 32 | AGGREGATE_TELEMETRY_TYPE: 'yamcs.aggregate', 33 | OPERATOR_STATUS_TYPE: 'yamcs.operatorStatus', 34 | MISSION_STATUS_TYPE: 'yamcs.missionStatus', 35 | POLL_QUESTION_TYPE: 'yamcs.pollQuestion', 36 | ALARMS_TYPE: 'yamcs.alarms', 37 | GLOBAL_STATUS_TYPE: 'yamcs.globalStatus' 38 | }; 39 | export const MDB_TYPE = 'yamcs.mdbchanges'; 40 | 41 | export const DATA_TYPES = { 42 | DATA_TYPE_COMMANDS: 'commands', 43 | DATA_TYPE_EVENTS: 'events', 44 | DATA_TYPE_TELEMETRY: 'parameters', 45 | DATA_TYPE_FAULTS: 'parameters', 46 | DATA_TYPE_REPLY: 'reply', 47 | DATA_TYPE_ALARMS: 'alarms', 48 | DATA_TYPE_GLOBAL_STATUS: 'global-alarm-status', 49 | DATA_TYPE_MDB_CHANGES: 'mdb-changes' 50 | }; 51 | 52 | export const STALENESS_STATUS_MAP = { 53 | 'ACQUIRED': false, 54 | 'EXPIRED': true 55 | }; 56 | 57 | export const SEVERITY_LEVELS = ['info', 'watch', 'warning', 'distress', 'critical', 'severe']; 58 | 59 | export const METADATA_TIME_KEY = 'generationTime'; 60 | 61 | export const UNSUPPORTED_TYPE = 'Unsupported Data Type'; 62 | export const AGGREGATE_TYPE = 'AGGREGATE'; 63 | export const NAMESPACE = 'taxonomy'; 64 | 65 | export const MDB_OBJECT = Object.freeze({ 66 | identifier: { 67 | namespace: NAMESPACE, 68 | key: MDB_TYPE 69 | }, 70 | type: MDB_TYPE 71 | }); 72 | 73 | export const MDB_CHANGES_PARAMETER_TYPE = 'PARAMETER'; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openmct-yamcs", 3 | "version": "4.0.0-next", 4 | "description": "An adapter for connecting Open MCT with YAMCS", 5 | "main": "dist/openmct-yamcs.js", 6 | "module": "dist/openmct-yamcs.js", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/openmct-yamcs.js", 10 | "require": "./dist/openmct-yamcs.js" 11 | } 12 | }, 13 | "workspaces": [ 14 | "tests/e2e/opensource" 15 | ], 16 | "scripts": { 17 | "clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./test-results ./tests/html-test-results ./tests/e2e/test-results ./.nyc_output ./tests/e2e/opensource", 18 | "test": "npm run test:getopensource && npm run test:e2e:smoke", 19 | "lint": "eslint src example", 20 | "lint:fix": "eslint src example --fix", 21 | "build:dist": "webpack --config ./.webpack/webpack.prod.mjs", 22 | "build:example": "npm install openmct@unstable --no-save", 23 | "build:example:master": "npm install nasa/openmct#master --no-save", 24 | "build:example:currentbranch": "npm install nasa/openmct#$(git rev-parse --abbrev-ref HEAD) --no-save --verbose", 25 | "postbuild:example": "node check-optional-dependencies.mjs", 26 | "start": "npx webpack serve --config ./.webpack/webpack.dev.mjs", 27 | "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.mjs", 28 | "prepare": "npm run build:dist", 29 | "test:getopensource": "sh ./tests/git-opensource-tests.sh", 30 | "posttest:getopensource": "npm install", 31 | "test:e2e:smoke": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=chromium quickstartSmoke", 32 | "test:e2e:quickstart": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=chromium tests/e2e/yamcs/", 33 | "test:e2e:quickstart:local": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=local-chrome tests/e2e/yamcs/", 34 | "test:e2e:watch": "npm test --workspace tests/e2e/opensource -- --ui --config=../playwright-quickstart.config.js", 35 | "wait-for-yamcs": "wait-on http-get://localhost:8090/ -v", 36 | "make-example-events": "node ./example/make-example-events.mjs" 37 | }, 38 | "keywords": [ 39 | "openmct", 40 | "yamcs" 41 | ], 42 | "author": "National Aeronautics and Space Administration", 43 | "license": "Apache-2.0", 44 | "engines": { 45 | "node": ">=18.14.2 <22" 46 | }, 47 | "optionalDependencies": { 48 | "openmct": ">= 3.9.9" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "7.20.12", 52 | "@babel/eslint-parser": "7.23.3", 53 | "babel-loader": "9.1.0", 54 | "babel-plugin-istanbul": "6.1.1", 55 | "eslint": "8.56.0", 56 | "eslint-plugin-import": "2.29.1", 57 | "eventemitter3": "4.0.7", 58 | "file-saver": "2.0.5", 59 | "semver": "7.5.2", 60 | "sinon": "14.0.0", 61 | "source-map-loader": "4.0.1", 62 | "uuid": "9.0.1", 63 | "wait-on": "7.0.1", 64 | "webpack": "5.90.3", 65 | "webpack-cli": "5.1.1", 66 | "webpack-dev-server": "5.0.2", 67 | "webpack-merge": "5.10.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/quickstartTools.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | async function disableLink(yamcsURL) { 23 | const url = new URL(`api/links/myproject/udp-in:disable`, yamcsURL); 24 | await fetch(url.toString(), { 25 | method: 'POST' 26 | }); 27 | } 28 | 29 | async function enableLink(yamcsURL) { 30 | const url = new URL(`api/links/myproject/udp-in:enable`, yamcsURL); 31 | await fetch(url.toString(), { 32 | method: 'POST' 33 | }); 34 | } 35 | 36 | async function isLinkEnabled(yamcsURL) { 37 | const url = new URL(`api/links/myproject/udp-in`, yamcsURL); 38 | const response = await (await fetch(url.toString())).json(); 39 | 40 | return response.disabled !== true; 41 | } 42 | 43 | async function latestParameterValues(parameterIds, yamcsURL) { 44 | const parameterIdsRequest = { 45 | fromCache: true, 46 | id: parameterIds.map(parameterName => { 47 | return { 48 | name: parameterName 49 | }; 50 | }) 51 | }; 52 | const parameterIdsRequestSerialized = JSON.stringify(parameterIdsRequest); 53 | const url = new URL('api/processors/myproject/realtime/parameters:batchGet', yamcsURL); 54 | const response = await (await fetch(url, { 55 | method: 'POST', 56 | headers: { 57 | 'Content-Type': 'application/json' 58 | }, 59 | body: parameterIdsRequestSerialized 60 | })).json(); 61 | 62 | return response.value; 63 | } 64 | 65 | async function parameterArchive({start, end, parameterId, yamcsURL}) { 66 | const url = new URL(`api/archive/myproject/parameters/${parameterId}`, `${yamcsURL}`); 67 | url.searchParams.set('start', start); 68 | url.searchParams.set('stop', end); 69 | 70 | const response = await (await fetch(url.toString())).json(); 71 | 72 | return response.parameter; 73 | } 74 | 75 | export { 76 | disableLink, 77 | enableLink, 78 | isLinkEnabled, 79 | latestParameterValues, 80 | parameterArchive 81 | }; 82 | -------------------------------------------------------------------------------- /src/providers/mission-status/mission-status-parameter.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | const MISSION_STATUS_TYPE = 'yamcs.missionStatus'; 24 | const MISSION_ACTION_NAMESPACE = 'OpenMCT:action'; 25 | 26 | /** 27 | * Check if the parameter is a mission status parameter 28 | * @param {Parameter} parameter 29 | * @returns {boolean} true if the parameter is a mission status parameter, false otherwise 30 | */ 31 | export function isMissionStatusParameter(parameter) { 32 | const aliases = parameter.alias; 33 | 34 | return aliases !== undefined 35 | && aliases.some(alias => alias.name === MISSION_STATUS_TYPE); 36 | } 37 | 38 | /** 39 | * Get the mission action from the parameter 40 | * @param {Parameter} parameter 41 | * @returns {import("./mission-status-telemetry").MissionAction? } the mission action name if the parameter is a mission action parameter, null otherwise 42 | */ 43 | export function getMissionActionFromParameter(parameter) { 44 | const aliases = parameter.alias; 45 | 46 | return aliases.find(alias => alias.namespace === MISSION_ACTION_NAMESPACE)?.name ?? null; 47 | } 48 | 49 | /** 50 | * Get the possible mission action statuses from the parameter 51 | * @param {Parameter} parameter 52 | * @returns {string[]} 53 | */ 54 | export function getPossibleMissionActionStatusesFromParameter(parameter) { 55 | return parameter.type.enumValue; 56 | } 57 | 58 | /** 59 | * @typedef {import("./mission-status-telemetry").MdbEntry} MdbEntry 60 | */ 61 | 62 | /** 63 | * @typedef {object} Parameter 64 | * @property {string} name 65 | * @property {string} qualifiedName 66 | * @property {object} type 67 | * @property {string} type.engType 68 | * @property {object} type.dataEncoding 69 | * @property {string} type.dataEncoding.type 70 | * @property {boolean} type.dataEncoding.littleEndian 71 | * @property {number} type.dataEncoding.sizeInBits 72 | * @property {string} type.dataEncoding.encoding 73 | * @property {MdbEntry[]} type.enumValue 74 | * @property {string} dataSource 75 | */ 76 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/load.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | Open MCT load Specific Tests 25 | */ 26 | 27 | import { pluginFixtures } from 'openmct-e2e'; 28 | const { test, expect } = pluginFixtures; 29 | const YAMCS_URL = 'http://localhost:8090/'; 30 | 31 | test.describe("Tests to ensure that open mct loads correctly @yamcs", () => { 32 | test.beforeEach(async ({ page }) => { 33 | await clearCustomAlgorithm(page); 34 | }); 35 | 36 | test.afterEach(async ({ page }) => { 37 | await clearCustomAlgorithm(page); 38 | }); 39 | 40 | test('Can load correctly when mdb algorithms are changed at runtime', async ({ page }) => { 41 | // Go to baseURL 42 | await page.goto("./", {waitUntil: "networkidle"}); 43 | await expect(page.getByLabel('Navigate to myproject folder')).toBeVisible(); 44 | 45 | await updateCustomAlgorithm(page); 46 | 47 | await page.reload({waitUntil: "networkidle"}); 48 | 49 | await expect(page.getByLabel('Navigate to myproject folder')).toBeVisible(); 50 | }); 51 | }); 52 | 53 | async function clearCustomAlgorithm(page) { 54 | // clear the custom algorithm for the copySunsensor using the yamcs API 55 | const runTimeCustomAlgorithmResetResponse = await page.request.patch(`${YAMCS_URL}api/mdb/myproject/realtime/algorithms/myproject/copySunsensor`, { 56 | data: { 57 | "action": "RESET" 58 | } 59 | }); 60 | await expect(runTimeCustomAlgorithmResetResponse).toBeOK(); 61 | } 62 | 63 | async function updateCustomAlgorithm(page) { 64 | // Change the custom algorithm for the copySunsensor using the yamcs API 65 | const runTimeCustomAlgorithmChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb/myproject/realtime/algorithms/myproject/copySunsensor`, { 66 | data: { 67 | "action": "SET", 68 | "algorithm": { 69 | "text": "\n\t\t\t\t\tout0.setFloatValue(in.getEngValue().getFloatValue()); \n\t\t\t\t" 70 | } 71 | } 72 | }); 73 | await expect(runTimeCustomAlgorithmChangeResponse).toBeOK(); 74 | } 75 | -------------------------------------------------------------------------------- /src/providers/user/createYamcsUser.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | export default function createYamcsUser(UserClass) { 24 | /** 25 | * @typedef {Object} YamcsUserInfo 26 | * @property {String} name 27 | * @property {Boolean} active 28 | * @property {Boolean} superuser 29 | * @property {String} creationTime an ISO 8601 string 30 | * @property {String} confirmationTime an ISO 8601 string 31 | * @property {String} lastLoginTime an ISO 8601 string 32 | * @property {String[]} roles 33 | * @property {ObjectPrivilege[]} objectPrivilege 34 | * See the Yamcs documentation for more details on the Yamcs User type 35 | * @see https://docs.yamcs.org/javadoc/yamcs/latest/org/yamcs/security/User.html 36 | */ 37 | /** 38 | * @typedef ObjectPrivilege 39 | * @property {String} type 40 | * @property {String[]} object An array of parameter identifiers 41 | */ 42 | return class YamcsUser extends UserClass { 43 | /** 44 | * @param {YamcsUserInfo} userInfo 45 | */ 46 | constructor({ 47 | name, 48 | active, 49 | superuser, 50 | creationTime, 51 | confirmationTime, 52 | lastLoginTime, 53 | roles = [], 54 | objectPrivilege 55 | }) { 56 | super(name, name); // id, name (yamcs doesn't provide an id) 57 | 58 | this.active = active; 59 | this.superuser = superuser; 60 | this.creationTime = creationTime; 61 | this.confirmationTime = confirmationTime; 62 | this.lastLoginTime = lastLoginTime; 63 | this.roles = roles.map(role => role.name); 64 | this.objectPrivileges = objectPrivilege; 65 | } 66 | 67 | getWriteParameters() { 68 | if (!this.objectPrivileges) { 69 | return []; 70 | } 71 | 72 | const writeParameters = this.objectPrivileges.find(entry => entry.type === 'WriteParameter')?.object || []; 73 | 74 | return writeParameters; 75 | } 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/quickstartTools.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import { 24 | enableLink, 25 | disableLink, 26 | isLinkEnabled, 27 | latestParameterValues, 28 | parameterArchive 29 | } from './quickstartTools.mjs'; 30 | import { pluginFixtures } from 'openmct-e2e'; 31 | const { test, expect } = pluginFixtures; 32 | 33 | test.describe('Quickstart library functions', () => { 34 | let yamcsURL; 35 | 36 | test.beforeEach(async ({page}) => { 37 | // Go to baseURL so we can get relative URL 38 | await page.goto('./', { waitUntil: 'domcontentloaded' }); 39 | yamcsURL = new URL('/yamcs-proxy/', page.url()).toString(); 40 | await enableLink(yamcsURL); 41 | }); 42 | test('Link can be disabled', async ({ page }) => { 43 | await disableLink(yamcsURL); 44 | expect(await isLinkEnabled(yamcsURL)).toBe(false); 45 | }); 46 | test('Link can be enabled', async ({ page }) => { 47 | await disableLink(yamcsURL); 48 | expect(await isLinkEnabled(yamcsURL)).toBe(false); 49 | 50 | await enableLink(yamcsURL); 51 | expect(await isLinkEnabled(yamcsURL)).toBe(true); 52 | }); 53 | test('Latest values can be retrieved', async () => { 54 | const latestValues = await latestParameterValues(['/myproject/Battery1_Temp', '/myproject/Battery1_Voltage'], yamcsURL); 55 | expect(latestValues.length).toBe(2); 56 | const areAllParameterValuesNumbers = latestValues.every((parameter) => { 57 | return !isNaN(parameter.engValue.floatValue); 58 | }); 59 | 60 | expect(areAllParameterValuesNumbers).toBe(true); 61 | }); 62 | test('Parameter archive values can be retrieved', async () => { 63 | const now = new Date(); 64 | const ONE_MINUTE = 60 * 1000; 65 | const then = new Date(now - ONE_MINUTE); 66 | const latestValues = await parameterArchive({ 67 | start: then.toISOString(), 68 | end: now.toISOString(), 69 | parameterId: '/myproject/Battery1_Temp', 70 | yamcsURL 71 | }); 72 | expect(latestValues.length).toBeGreaterThan(0); 73 | 74 | const areAllParameterValuesNumbers = latestValues.every((parameter) => { 75 | return !isNaN(parameter.engValue.floatValue); 76 | }); 77 | 78 | expect(areAllParameterValuesNumbers).toBe(true); 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /src/providers/user/poll-question-telemetry.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | import { 23 | idToQualifiedName 24 | } from '../../utils.js'; 25 | 26 | export default class PollQuestionTelemetry { 27 | #setReady; 28 | #readyPromise; 29 | #url; 30 | #instance; 31 | #processor; 32 | #openmct; 33 | #telemetryObject; 34 | 35 | constructor(openmct, {url, instance, processor = 'realtime'}) { 36 | this.#readyPromise = new Promise((resolve) => this.#setReady = resolve); 37 | this.#url = url; 38 | this.#instance = instance; 39 | this.#processor = processor; 40 | this.#openmct = openmct; 41 | } 42 | setTelemetryObject(telemetryObject) { 43 | this.#telemetryObject = telemetryObject; 44 | this.#setReady(); 45 | } 46 | async getTelemetryObject() { 47 | await this.#readyPromise; 48 | 49 | return this.#telemetryObject; 50 | } 51 | async setPollQuestion(question) { 52 | const telemetryObject = await this.getTelemetryObject(); 53 | const setParameterUrl = this.#buildUrl(telemetryObject.identifier); 54 | let success = false; 55 | 56 | try { 57 | const result = await fetch(setParameterUrl, { 58 | method: 'PUT', 59 | headers: { 60 | 'Content-Type': 'application/json' 61 | }, 62 | body: JSON.stringify({ 63 | type: 'STRING', 64 | stringValue: question 65 | }) 66 | }); 67 | success = result.ok === true; 68 | } catch (error) { 69 | console.error(error); 70 | } 71 | 72 | return success; 73 | } 74 | toPollQuestionObjectFromTelemetry(telemetryObject, datum) { 75 | const metadata = this.#openmct.telemetry.getMetadata(telemetryObject); 76 | const questionMetadata = metadata.getDefaultDisplayValue(); 77 | const timestampMetadata = metadata.valuesForHints(['domain'])[0]; 78 | const questionFormatter = this.#openmct.telemetry.getValueFormatter(questionMetadata); 79 | const dateFormatter = this.#openmct.telemetry.getValueFormatter(timestampMetadata); 80 | 81 | return { 82 | timestamp: dateFormatter.parse(datum), 83 | question: questionFormatter.parse(datum) 84 | }; 85 | 86 | } 87 | #buildUrl(id) { 88 | let url = `${this.#url}api/processors/${this.#instance}/${this.#processor}/parameters/${idToQualifiedName(id.key)}`; 89 | 90 | return url; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/providers/fault-mgmt-providers/fault-action-provider.js: -------------------------------------------------------------------------------- 1 | import { FAULT_MGMT_ALARMS, FAULT_MGMT_ACTIONS } from './fault-mgmt-constants.js'; 2 | 3 | export default class FaultActionProvider { 4 | constructor(url, instance, processor) { 5 | this.url = url; 6 | this.instance = instance; 7 | this.processor = processor; 8 | } 9 | 10 | acknowledgeFault(fault, { comment = '' } = {}) { 11 | const payload = { 12 | comment 13 | }; 14 | const options = this.#getOptions(payload); 15 | const url = this.#getUrl(fault, FAULT_MGMT_ACTIONS.ACKNOWLEDGE); 16 | 17 | return this.#sendRequest(url, options); 18 | } 19 | 20 | /** 21 | * Shelves or unshelves a fault. 22 | * @param {FaultModel} fault the fault to perform the action on 23 | * @param {Object} options the options to perform the action with 24 | * @param {boolean} options.shelved whether to shelve or unshelve the fault 25 | * @param {string} options.comment the comment to add to the fault 26 | * @param {number} options.shelveDuration the duration to shelve the fault for 27 | * @returns {Promise} the response from the server 28 | */ 29 | shelveFault(fault, { shelved = true, comment = '', shelveDuration } = {}) { 30 | const payload = {}; 31 | const action = shelved ? FAULT_MGMT_ACTIONS.SHELVE : FAULT_MGMT_ACTIONS.UNSHELVE; 32 | 33 | if (shelved) { 34 | payload.comment = comment; 35 | payload.shelveDuration = shelveDuration; 36 | } 37 | 38 | const options = this.#getOptions(payload); 39 | const url = this.#getUrl(fault, action); 40 | 41 | return this.#sendRequest(url, options); 42 | } 43 | 44 | /** 45 | * @typedef {Object} ShelveDuration 46 | * @property {string} name - The name of the shelve duration 47 | * @property {number|null} value - The value of the shelve duration in milliseconds, or null for unlimited 48 | */ 49 | 50 | /** 51 | * @returns {ShelveDuration[]} the list of shelve durations 52 | */ 53 | getShelveDurations() { 54 | return [ 55 | { 56 | name: '15 Minutes', 57 | value: 1000 * 60 * 15 58 | }, 59 | { 60 | name: '30 Minutes', 61 | value: 1000 * 60 * 30 62 | }, 63 | { 64 | name: '1 Hour', 65 | value: 1000 * 60 * 60 66 | }, 67 | { 68 | name: '2 Hours', 69 | value: 1000 * 60 * 60 * 2 70 | }, 71 | { 72 | name: '1 Day', 73 | value: 1000 * 60 * 60 * 24 74 | }, 75 | { 76 | name: 'Unlimited', 77 | value: null 78 | } 79 | ]; 80 | } 81 | 82 | #getOptions(payload) { 83 | return { 84 | body: JSON.stringify(payload), 85 | headers: { 86 | 'Content-Type': 'application/json' 87 | }, 88 | method: 'POST', 89 | mode: 'cors' 90 | }; 91 | } 92 | 93 | /** 94 | * @param {FaultModel} fault the fault to perform the action on 95 | * @param {'acknowledge' | 'shelve' | 'unshelve' | 'clear'} action the action to perform on the fault 96 | * @returns {string} the URL to perform the action on the fault 97 | */ 98 | #getUrl(fault, action) { 99 | return `${this.url}api/processors/${this.instance}/${this.processor}/${FAULT_MGMT_ALARMS}` 100 | + `${fault.namespace}/${fault.name}/${fault.seqNum}:${action}`; 101 | } 102 | 103 | #sendRequest(url, options) { 104 | return fetch(url, options); 105 | } 106 | } 107 | 108 | /** @typedef {import('./utils.js').FaultModel} FaultModel */ 109 | -------------------------------------------------------------------------------- /src/providers/messages.js: -------------------------------------------------------------------------------- 1 | import {OBJECT_TYPES, DATA_TYPES, MDB_TYPE} from '../const.js'; 2 | 3 | const typeMap = { 4 | [OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_COMMANDS, 5 | [OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_COMMANDS, 6 | [OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_EVENTS, 7 | [OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_EVENTS, 8 | [OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_EVENTS, 9 | [OBJECT_TYPES.TELEMETRY_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, 10 | [OBJECT_TYPES.STRING_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, 11 | [OBJECT_TYPES.IMAGE_OBJECT_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, 12 | [OBJECT_TYPES.AGGREGATE_TELEMETRY_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, 13 | [OBJECT_TYPES.OPERATOR_STATUS_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, 14 | [OBJECT_TYPES.MISSION_STATUS_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, 15 | [OBJECT_TYPES.POLL_QUESTION_TYPE]: DATA_TYPES.DATA_TYPE_TELEMETRY, 16 | [OBJECT_TYPES.ALARMS_TYPE]: DATA_TYPES.DATA_TYPE_ALARMS, 17 | [OBJECT_TYPES.GLOBAL_STATUS_TYPE]: DATA_TYPES.DATA_TYPE_GLOBAL_STATUS, 18 | [MDB_TYPE]: DATA_TYPES.DATA_TYPE_MDB_CHANGES 19 | }; 20 | 21 | export const SUBSCRIBE = buildSubscribeMessages(); 22 | // eslint-disable-next-line func-style 23 | export const UNSUBSCRIBE = (subscriptionDetails) => { 24 | return `{ 25 | "type": "cancel", 26 | "options": { 27 | "call": "${subscriptionDetails.call}" 28 | } 29 | }`; 30 | }; 31 | 32 | function buildSubscribeMessages() { 33 | let subscriptionMessages = {}; 34 | 35 | for (const [objectType, dataType] of Object.entries(typeMap)) { 36 | 37 | subscriptionMessages[objectType] = (subscriptionDetails) => { 38 | const {subscriptionId, instance, processor = "realtime", name } = subscriptionDetails; 39 | let message; 40 | 41 | if (isEventType(objectType)) { 42 | message = `{ 43 | "type": "${dataType}", 44 | "id": "${subscriptionId}", 45 | "options": { 46 | "instance": "${instance}" 47 | } 48 | }`; 49 | } else if (isAlarmType(objectType) || isCommandType(objectType) || isMdbChangesType(objectType)) { 50 | message = `{ 51 | "type": "${dataType}", 52 | "id": "${subscriptionId}", 53 | "options": { 54 | "instance": "${instance}", 55 | "processor": "${processor}" 56 | } 57 | }`; 58 | } else { 59 | message = `{ 60 | "type": "${dataType}", 61 | "id": "${subscriptionId}", 62 | "options": { 63 | "instance": "${instance}", 64 | "processor": "${processor}", 65 | "id": [{ 66 | "name": "${name}" 67 | }], 68 | "sendFromCache": true, 69 | "updateOnExpiration": true 70 | } 71 | }`; 72 | } 73 | 74 | return message; 75 | }; 76 | } 77 | 78 | return subscriptionMessages; 79 | } 80 | 81 | function isEventType(type) { 82 | return [OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE].includes(type); 83 | } 84 | 85 | function isAlarmType(type) { 86 | return type === OBJECT_TYPES.ALARMS_TYPE 87 | || type === OBJECT_TYPES.GLOBAL_STATUS_TYPE; 88 | } 89 | 90 | function isCommandType(type) { 91 | return type === OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE || type === OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE; 92 | } 93 | 94 | function isMdbChangesType(type) { 95 | return type === MDB_TYPE; 96 | } 97 | -------------------------------------------------------------------------------- /example/make-example-events.mjs: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | 3 | const INSTANCE = "myproject"; 4 | const URL = `http://localhost:8090/api/archive/${INSTANCE}/events`; 5 | 6 | const events = [ 7 | { 8 | type: "PRESSURE_ALERT", 9 | message: "Pressure threshold exceeded", 10 | severity: "CRITICAL", 11 | source: "PressureModule", 12 | sequenceNumber: 1, 13 | extra: { 14 | pressure: "150 PSI", 15 | location: "Hydraulic System" 16 | } 17 | }, 18 | { 19 | type: "PRESSURE_WARNING", 20 | message: "Pressure nearing critical level", 21 | severity: "WARNING", 22 | source: "PressureModule", 23 | sequenceNumber: 2, 24 | extra: { 25 | pressure: "140 PSI", 26 | location: "Hydraulic System" 27 | } 28 | }, 29 | { 30 | type: "PRESSURE_INFO", 31 | message: "Pressure system check completed", 32 | severity: "INFO", 33 | source: "PressureModule", 34 | sequenceNumber: 3, 35 | extra: { 36 | checkType: "Routine Inspection", 37 | duration: "10m" 38 | } 39 | }, 40 | { 41 | type: "TEMPERATURE_ALERT", 42 | message: "Temperature threshold exceeded", 43 | severity: "CRITICAL", 44 | source: "TemperatureModule", 45 | sequenceNumber: 4, 46 | extra: { 47 | temperature: "100°C", 48 | location: "Engine Room" 49 | } 50 | }, 51 | { 52 | type: "TEMPERATURE_WARNING", 53 | message: "Temperature nearing critical level", 54 | severity: "WARNING", 55 | source: "TemperatureModule", 56 | sequenceNumber: 5, 57 | extra: { 58 | temperature: "95°C", 59 | location: "Engine Room" 60 | } 61 | }, 62 | { 63 | type: "TEMPERATURE_INFO", 64 | message: "Temperature nominal", 65 | severity: "INFO", 66 | source: "TemperatureModule", 67 | sequenceNumber: 6, 68 | extra: { 69 | temperature: "35°C", 70 | location: "Life Support" 71 | } 72 | }, 73 | { 74 | type: "TEMPERATURE_INFO", 75 | message: "Temperature nominal", 76 | severity: "INFO", 77 | source: "TemperatureModule", 78 | sequenceNumber: 7, 79 | extra: { 80 | temperature: "30°C", 81 | location: "Life Support" 82 | } 83 | }, 84 | { 85 | type: "TEMPERATURE_SEVERE", 86 | message: "Temperature nominal", 87 | severity: "SEVERE", 88 | source: "TemperatureModule", 89 | sequenceNumber: 8, 90 | extra: { 91 | temperature: "200°C", 92 | location: "Engine Room" 93 | } 94 | } 95 | ]; 96 | 97 | async function postEvent(event, delaySeconds) { 98 | const eventTime = new Date(Date.now() + delaySeconds * 1000).toISOString(); 99 | event.time = eventTime; 100 | 101 | try { 102 | const response = await fetch(URL, { 103 | method: 'POST', 104 | headers: { 'Content-Type': 'application/json' }, 105 | body: JSON.stringify(event) 106 | }); 107 | 108 | if (response.ok) { 109 | console.log(`Event posted successfully: ${event.type}`); 110 | } else { 111 | console.error(`Failed to post event: ${event.type}. HTTP Status: ${response.status}`); 112 | } 113 | } catch (error) { 114 | console.error(`Error posting event: ${event.type}.`, error); 115 | } 116 | } 117 | 118 | export async function postAllEvents() { 119 | for (let i = 0; i < events.length; i++) { 120 | await postEvent(events[i], i * 5); 121 | } 122 | } 123 | 124 | // If you still want to run it standalone 125 | if (import.meta.url === `file://${process.argv[1]}`) { 126 | postAllEvents(); 127 | } 128 | -------------------------------------------------------------------------------- /src/providers/fault-mgmt-providers/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-style */ 2 | /***************************************************************************** 3 | * Open MCT, Copyright (c) 2014-2021, United States Government 4 | * as represented by the Administrator of the National Aeronautics and Space 5 | * Administration. All rights reserved. 6 | * 7 | * Open MCT is licensed under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * http://www.apache.org/licenses/LICENSE-2.0. 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations 16 | * under the License. 17 | * 18 | * Open MCT includes source code licensed under additional open source 19 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 20 | * this source code distribution or the Licensing information page available 21 | * at runtime from the About dialog for additional information. 22 | *****************************************************************************/ 23 | import { getValue } from '../../utils.js'; 24 | 25 | /** 26 | * Converts fault data to a FaultModel. 27 | * 28 | * @param {Object} faultData 29 | * @param {string} [type] 30 | * @returns {FaultModel} 31 | */ 32 | const convertDataToFaultModel = (faultData, type) => { 33 | const parameterDetail = faultData?.parameterDetail; 34 | const currentValueDetail = parameterDetail?.currentValue; 35 | const triggerValueDetail = parameterDetail?.triggerValue; 36 | 37 | const currentValue = currentValueDetail ? getValue(currentValueDetail) : undefined; 38 | const triggerValue = triggerValueDetail ? getValue(triggerValueDetail) : undefined; 39 | 40 | return { 41 | type: type || faultData?.type, 42 | fault: { 43 | acknowledged: Boolean(faultData?.acknowledged), 44 | currentValueInfo: { 45 | value: currentValue, 46 | rangeCondition: currentValueDetail?.rangeCondition, 47 | monitoringResult: currentValueDetail?.monitoringResult 48 | }, 49 | id: `id-${faultData?.id?.namespace}-${faultData?.id?.name}`, 50 | name: faultData?.id?.name, 51 | namespace: faultData?.id?.namespace, 52 | seqNum: faultData?.seqNum, 53 | severity: faultData?.severity, 54 | shelved: Boolean(faultData?.shelveInfo), 55 | shortDescription: parameterDetail?.parameter?.shortDescription, 56 | triggerTime: faultData?.triggerTime, 57 | triggerValueInfo: { 58 | value: triggerValue, 59 | rangeCondition: triggerValueDetail?.rangeCondition, 60 | monitoringResult: triggerValueDetail?.monitoringResult 61 | } 62 | } 63 | }; 64 | }; 65 | 66 | export { convertDataToFaultModel }; 67 | 68 | /** 69 | * @typedef {Object} FaultModel 70 | * @property {string} type 71 | * @property {Object} fault 72 | * @property {boolean} fault.acknowledged 73 | * @property {Object} fault.currentValueInfo 74 | * @property {*} fault.currentValueInfo.value 75 | * @property {string} fault.currentValueInfo.rangeCondition 76 | * @property {string} fault.currentValueInfo.monitoringResult 77 | * @property {string} fault.id 78 | * @property {string} fault.name 79 | * @property {string} fault.namespace 80 | * @property {number} fault.seqNum 81 | * @property {string} fault.severity 82 | * @property {boolean} fault.shelved 83 | * @property {string} fault.shortDescription 84 | * @property {number} fault.triggerTime 85 | * @property {Object} fault.triggerValueInfo 86 | * @property {*} fault.triggerValueInfo.value 87 | * @property {string} fault.triggerValueInfo.rangeCondition 88 | * @property {string} fault.triggerValueInfo.monitoringResult 89 | */ 90 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/filters.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | Filter Specific Tests 25 | */ 26 | 27 | import { pluginFixtures, appActions } from 'openmct-e2e'; 28 | const { test, expect } = pluginFixtures; 29 | const { createDomainObjectWithDefaults } = appActions; 30 | 31 | test.describe("Filter tests @yamcs", () => { 32 | test('Can filter events by severity', async ({ page }) => { 33 | // Go to baseURL 34 | await page.goto("./", { waitUntil: "networkidle" }); 35 | const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); 36 | const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); 37 | await firstMyProjectTriangle.click(); 38 | const eventsTreeItem = page.getByRole('treeitem', { name: /Events/ }); 39 | await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' }); 40 | 41 | const objectPane = page.locator('.c-object-view'); 42 | await eventsTreeItem.dragTo(objectPane); 43 | 44 | // ensure global filters work 45 | await page.getByRole('tab', { name: 'Filters' }).click(); 46 | await page.getByRole('listitem').filter({ hasText: 'Global Filtering' }).locator('span').click(); 47 | await page.getByRole('listitem').filter({ hasText: 'Events' }).locator('span').click(); 48 | await page.locator('[aria-label="Global Filter"]').selectOption('critical'); 49 | await expect(page.getByText('Filters applied')).toBeVisible(); 50 | await expect(page.getByTitle('Data filters are being applied to this view.').getByText('critical')).toBeVisible(); 51 | await page.locator('[aria-label="Global Filter"]').selectOption('NONE'); 52 | await expect(page.getByText('Filters applied')).toBeHidden(); 53 | await expect(page.getByTitle('Data filters are being applied to this view.')).toBeHidden(); 54 | 55 | // ensure specific object filters work 56 | await page.getByRole('switch').click(); 57 | await page.locator('[aria-label="Specific Filter"]').selectOption('info'); 58 | await expect(page.getByText('Filters applied')).toBeVisible(); 59 | await expect(page.getByTitle('Data filters are being applied to this view.').getByText('info')).toBeVisible(); 60 | await page.locator('[aria-label="Specific Filter"]').selectOption('NONE'); 61 | await expect(page.getByText('Filters applied')).toBeHidden(); 62 | await expect(page.getByTitle('Data filters are being applied to this view.')).toBeHidden(); 63 | 64 | // ensure specific object filters override global filters 65 | await page.locator('[aria-label="Specific Filter"]').selectOption('info'); 66 | await page.locator('[aria-label="Global Filter"]').selectOption('critical'); 67 | await expect(page.getByText('Filters applied')).toBeVisible(); 68 | await expect(page.getByTitle('Data filters are being applied to this view.').getByText('info')).toBeVisible(); 69 | 70 | // ensure global filters override when switch is on 71 | await page.getByRole('switch').click(); 72 | await expect(page.getByText('Filters applied')).toBeVisible(); 73 | await expect(page.getByTitle('Data filters are being applied to this view.').getByText('critical')).toBeVisible(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/providers/limit-provider.js: -------------------------------------------------------------------------------- 1 | /* CSS classes for Yamcs parameter monitoring result values. */ 2 | 3 | const MONITORING_RESULT_CSS = { 4 | 'WATCH': 'is-limit--yellow', 5 | 'WARNING': 'is-limit--yellow', 6 | 'DISTRESS': 'is-limit--red', 7 | 'CRITICAL': 'is-limit--red', 8 | 'SEVERE': 'is-limit--red' 9 | }; 10 | 11 | /* CSS classes for Yamcs range condition values. */ 12 | const RANGE_CONDITION_CSS = { 13 | 'LOW': 'is-limit--lwr', 14 | 'HIGH': 'is-limit--upr' 15 | }; 16 | 17 | /** 18 | * @typedef {Object} Datum 19 | * @property {number} [monitoringResult] the Yamcs limit monitoring result 20 | * @property {number} [rangeCondition] the Yamcs range condition (LOW/HIGH) 21 | * @property {Array} alarmRange alarm ranges for different monitoringresults, or omitted, if no alarm ranges are defined 22 | * A floating point value representing some observable quantity (eg.temperature, air pressure, etc.) 23 | */ 24 | 25 | /** 26 | * @typedef {Object} EvaluationResult 27 | * @property {string} cssClass CSS class information 28 | * @property {string} name a violation name 29 | * @property {number} low a lower limit for violation 30 | * @property {number} high a higher limit violation 31 | */ 32 | export default class LimitProvider { 33 | constructor(openmct, url, instance, realtimeTelemetryProvider) { 34 | this.openmct = openmct; 35 | this.realtimeTelemetryProvider = realtimeTelemetryProvider; 36 | this.url = url; 37 | this.instance = instance; 38 | } 39 | 40 | getLimitEvaluator(domainObject) { 41 | const self = this; 42 | 43 | return { 44 | /** 45 | * Evaluates a telemetry datum for limit violations. 46 | * 47 | * @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum}) 48 | * @param {object} valueMetadata metadata about the telemetry datum 49 | * 50 | * @returns {EvaluationResult} ({@link EvaluationResult}) 51 | */ 52 | evaluate: function (datum, valueMetadata) { 53 | if (valueMetadata && datum.monitoringResult 54 | && datum.monitoringResult in MONITORING_RESULT_CSS) { 55 | 56 | const evaluationResult = self.getLimitInfo(datum, datum.monitoringResult, valueMetadata); 57 | 58 | return evaluationResult; 59 | } 60 | } 61 | }; 62 | } 63 | 64 | /** 65 | * Adds limit range information to an object based on the monitoring 66 | * result. 67 | * 68 | * @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum}) 69 | * @param {string} result the monitoring result information from Yamcs 70 | * @param {object} [valueMetadata] metadata about the telemetry datum 71 | * 72 | * @returns {EvaluationResult} ({@link EvaluationResult}) 73 | */ 74 | getLimitInfo(datum, result, valueMetadata) { 75 | if (!valueMetadata) { 76 | return undefined; 77 | } 78 | 79 | if (valueMetadata.key === 'value') { 80 | let cssClass = MONITORING_RESULT_CSS[result]; 81 | 82 | // Include the rangeCondition that's being violated if it is available. 83 | // Example: For enums the rangeCondition (upper or lower) does not make sense. So we skip it. 84 | if (datum.rangeCondition 85 | && datum.rangeCondition in RANGE_CONDITION_CSS) { 86 | cssClass = ' ' + RANGE_CONDITION_CSS[datum.rangeCondition]; 87 | } 88 | 89 | return { 90 | cssClass, 91 | name: result, 92 | low: Number.NEGATIVE_INFINITY, 93 | high: Number.POSITIVE_INFINITY 94 | }; 95 | } 96 | 97 | return undefined; 98 | } 99 | 100 | supportsLimits(domainObject) { 101 | if (domainObject.type.startsWith('yamcs.commands')) { 102 | return false; 103 | } 104 | 105 | return domainObject.type.startsWith('yamcs.'); 106 | } 107 | 108 | getLimits(domainObject) { 109 | return { 110 | limits: () => Promise.resolve(domainObject.configuration.limits) 111 | }; 112 | } 113 | 114 | subscribeToLimits(domainObject, callback) { 115 | return this.realtimeTelemetryProvider.subscribeToLimits(domainObject, callback); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | This test suite is dedicated to tests which can quickly verify that any openmct installation is 25 | operable and that any type of testing can proceed. 26 | 27 | Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them 28 | more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly 29 | as they cover a very "thin surface" of functionality. 30 | 31 | When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel 32 | comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects. 33 | Make no assumptions about the order that elements appear in the DOM. 34 | */ 35 | 36 | import { baseFixtures } from 'openmct-e2e'; 37 | const { test, expect } = baseFixtures; 38 | 39 | test.describe("Quickstart smoke tests @yamcs", () => { 40 | test.use({ 41 | storageState: { 42 | cookies: [], 43 | origins: [ 44 | { 45 | "origin": "http://localhost:9000", 46 | "localStorage": [] 47 | } 48 | ] 49 | } 50 | }); 51 | 52 | test.beforeEach(async ({ page }) => { 53 | //Go to baseURL 54 | await page.goto('./', { waitUntil: 'networkidle' }); 55 | }); 56 | 57 | test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => { 58 | //Click the Create button 59 | await page.getByRole('button', { name: 'Create' }).click(); 60 | 61 | // Verify that Create Folder appears in the dropdown 62 | await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); 63 | }); 64 | 65 | test('Verify that the default yamcs items appear in the tree', async ({ page }) => { 66 | //Go to baseURL 67 | await page.goto('./', { waitUntil: 'networkidle' }); 68 | 69 | // Check for existence of all default YAMCS tree objects 70 | await expect(page.locator('.c-tree__item :text-is("Fault Management")')).toBeEnabled(); 71 | await expect(page.locator('.c-tree__item :text-is("myproject")')).toBeEnabled(); 72 | await expect(page.locator('.c-tree__item :text-is("My Items")')).toBeEnabled(); 73 | }); 74 | 75 | test('Verify that the default display is generated and navigated to without error', async ({ page }) => { 76 | await expect(page.getByRole('main').getByText('Example Flexible Layout')).toBeVisible(); 77 | await expect(page.getByLabel('Health tab')).toBeVisible(); 78 | await expect(page.getByLabel('Position tab')).toBeVisible(); 79 | await expect(page.getByLabel('Velocity tab')).toBeVisible(); 80 | await expect(page.getByLabel('All Current Values tab')).toBeVisible(); 81 | await expect(page.getByText('SUBSYS')).toBeVisible(); 82 | await expect(page.getByText('ADCS', { exact: true })).toBeVisible(); 83 | await expect(page.getByText('CDHS', { exact: true })).toBeVisible(); 84 | await expect(page.getByText('COMMS', { exact: true })).toBeVisible(); 85 | await expect(page.getByText('EPS', { exact: true })).toBeVisible(); 86 | await expect(page.getByLabel('CW PYLD Status Object View').getByText('PAYLOAD')).toBeVisible(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/providers/mission-status/README.md: -------------------------------------------------------------------------------- 1 | # Mission Status 2 | 3 | ## Description 4 | 5 | "Mission Status" is a feature that is used to indicate the current state of a mission with regards to one or more "Mission Actions". A mission action defines a verb that may be, for example, a task for a spacecraft (such as "Drive" or "Imagery"), a change in the state of a ground system, or any other event that is relevant to the mission. Example states for a mission action might include "Go" or "No Go", indicating whether a particular action is currently cleared for execution. 6 | 7 | 8 | 9 | ## Setup 10 | 11 | In order to use this feature, YAMCS MDB and `roles.yaml` must be configured properly. 12 | 13 | ### YAMCS MDB 14 | 15 | In the YAMCS MDB, the Mission Status parameters must be defined. 16 | See the example below for a `SpaceSystem` named "OpenMCTTest" with three Mission Status parameters (actions): "Driving", "Drilling", and "Imagery". 17 | The possible values for each parameter are "NO GO" and "GO". 18 | 19 | * The `OpenMCT:type` alias is used to define the Parameter as a "Mission Status" parameter. 20 | * The `OpenMCT:action` alias is used to define the name of the Mission Action itself (e.g. "drivingStatus"). 21 | 22 | ```xml 23 | 24 | 25 | 29 | 30 | 31 | Parameters that are used for Open MCT testing only. 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ``` 75 | 76 | ### `roles.yaml` 77 | 78 | In the `roles.yaml` file, the user with permission to set mission status must be given `WriteParameter` permission to the path(s) which 79 | contain the Mission Status parameters: 80 | 81 | ```yaml 82 | # roles.yaml example granting the "Flight" role permission to set mission status 83 | Flight: 84 | Command: [] 85 | CommandHistory: [ ".*" ] 86 | ManageBucket: [] 87 | ReadAlgorithm: [ ".*" ] 88 | ReadBucket: [ ".*" ] 89 | ReadPacket: [ ".*" ] 90 | ReadParameter: [ ".*" ] 91 | Stream: [] 92 | WriteParameter: 93 | - "/MyProject/MissionStatus/.*" 94 | System: 95 | - GetMissionDatabase 96 | - ReadAlarms 97 | - ReadCommandHistory 98 | - ReadEvents 99 | - ReadFileTransfers 100 | - ReadLinks 101 | ``` 102 | 103 | ### User Provider 104 | 105 | See the [Open MCT documentation](https://github.com/nasa/openmct/blob/634aeef06e8712d3806bcd15fa9e5901386e12b3/src/plugins/userIndicator/README.md) for information on how to configure the User Provider to support Mission Status. 106 | -------------------------------------------------------------------------------- /src/providers/latest-telemetry-provider.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | import { 23 | getValue, 24 | idToQualifiedName, 25 | qualifiedNameFromParameterId, 26 | qualifiedNameToId 27 | } from '../utils.js'; 28 | 29 | const BATCH_DEBOUNCE_MS = 100; 30 | 31 | export default class LatestTelemetryProvider { 32 | #bulkPromise; 33 | #batchIds; 34 | #openmct; 35 | 36 | constructor({url, instance, processor = 'realtime', openmct}) { 37 | this.url = url; 38 | this.instance = instance; 39 | this.processor = processor; 40 | this.#batchIds = []; 41 | this.#openmct = openmct; 42 | } 43 | async requestLatest(domainObject) { 44 | const yamcsId = idToQualifiedName(domainObject.identifier.key); 45 | this.#batchIds.push(yamcsId); 46 | 47 | if (this.#bulkPromise === undefined) { 48 | this.#bulkPromise = this.#deferBatchedGet(); 49 | } 50 | 51 | try { 52 | const datumMap = await this.#bulkPromise; 53 | const result = datumMap[yamcsId]; 54 | 55 | let openMctStyleDatum = undefined; 56 | 57 | if (result !== undefined) { 58 | if (result.acquisitionStatus !== undefined) { 59 | const id = qualifiedNameFromParameterId(result.id); 60 | openMctStyleDatum = { 61 | id: qualifiedNameToId(id), 62 | acquisitionStatus: result.acquisitionStatus, 63 | timestamp: result.generationTime, 64 | value: getValue(result) 65 | }; 66 | } 67 | } 68 | 69 | return openMctStyleDatum; 70 | } catch (error) { 71 | console.error(error); 72 | this.#openmct.notifications.error(`Unable to fetch latest telemetry for ${domainObject.name}`); 73 | 74 | return undefined; 75 | } 76 | } 77 | async #deferBatchedGet() { 78 | // We until the next event loop cycle to "collect" all of the get 79 | // requests triggered in this iteration of the event loop 80 | 81 | await this.#waitForDebounce(); 82 | let batchIds = [...new Set(this.#batchIds)]; 83 | 84 | this.#clearBatch(); 85 | 86 | return this.#bulkGet(batchIds); 87 | 88 | } 89 | async #bulkGet(batchIds) { 90 | const yamcsIds = batchIds.map((yamcsId) => { 91 | return { 92 | name: yamcsId 93 | }; 94 | }); 95 | 96 | const requestBody = { 97 | id: yamcsIds, 98 | fromCache: true 99 | }; 100 | 101 | const response = await fetch(this.#buildUrl(), { 102 | method: 'POST', 103 | body: JSON.stringify(requestBody) 104 | }); 105 | 106 | const json = await response.json(); 107 | 108 | if (json.value !== undefined) { 109 | return json.value.reduce((map, parameterValue) => { 110 | map[parameterValue.id.name] = parameterValue; 111 | 112 | return map; 113 | }, {}); 114 | } else { 115 | return {}; 116 | } 117 | } 118 | 119 | #clearBatch() { 120 | this.#batchIds = []; 121 | this.#bulkPromise = undefined; 122 | } 123 | 124 | #waitForDebounce() { 125 | let timeoutID; 126 | clearTimeout(timeoutID); 127 | 128 | return new Promise((resolve) => { 129 | timeoutID = setTimeout(() => { 130 | resolve(); 131 | }, BATCH_DEBOUNCE_MS); 132 | }); 133 | } 134 | 135 | #buildUrl() { 136 | let url = `${this.url}api/processors/${this.instance}/${this.processor}/parameters:batchGet`; 137 | 138 | return url; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/providers/user/operator-status-telemetry.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | import { 23 | idToQualifiedName 24 | } from '../../utils.js'; 25 | 26 | export default class OperatorStatusTelemetry { 27 | #statusMap; 28 | #statusRoles; 29 | #roleToTelemetryObjectMap; 30 | #setReady; 31 | #readyPromise; 32 | #url; 33 | #instance; 34 | #processor; 35 | #openmct; 36 | 37 | constructor(openmct, {url, instance, processor = 'realtime'}) { 38 | this.#statusMap = {}; 39 | this.#statusRoles = new Set(); 40 | this.#roleToTelemetryObjectMap = {}; 41 | this.#readyPromise = new Promise((resolve) => this.#setReady = resolve); 42 | this.#url = url; 43 | this.#instance = instance; 44 | this.#processor = processor; 45 | this.#openmct = openmct; 46 | } 47 | async setStatusForRole(role, status) { 48 | const telemetryObject = await this.getTelemetryObjectForRole(role); 49 | const setParameterUrl = this.#buildUrl(telemetryObject.identifier); 50 | let success = false; 51 | 52 | try { 53 | const result = await fetch(setParameterUrl, { 54 | method: 'PUT', 55 | headers: { 56 | 'Content-Type': 'application/json' 57 | }, 58 | body: JSON.stringify({ 59 | type: 'SINT64', 60 | sint64Value: status.key 61 | }) 62 | }); 63 | 64 | success = result.ok === true; 65 | } catch (error) { 66 | console.error(error); 67 | } 68 | 69 | return success; 70 | } 71 | async getPossibleStatuses() { 72 | await this.#readyPromise; 73 | 74 | return Object.values(this.#statusMap).map(status => this.toStatusFromMdbEntry(status)); 75 | } 76 | addStatus(status) { 77 | this.#statusMap[status.value] = status; 78 | } 79 | async getTelemetryObjectForRole(role) { 80 | await this.#readyPromise; 81 | 82 | return this.#roleToTelemetryObjectMap[role]; 83 | } 84 | setTelemetryObjectForRole(role, telemetryObject) { 85 | this.#roleToTelemetryObjectMap[role] = telemetryObject; 86 | } 87 | addStatusRole(role) { 88 | this.#statusRoles.add(role); 89 | } 90 | async getAllStatusRoles() { 91 | await this.#readyPromise; 92 | 93 | return Array.from(this.#statusRoles); 94 | } 95 | async getDefaultStatusForRole() { 96 | const possibleStatuses = await this.getPossibleStatuses(); 97 | 98 | return possibleStatuses[0]; 99 | } 100 | toStatusFromMdbEntry(yamcsStatus) { 101 | return { 102 | // eslint-disable-next-line radix 103 | key: parseInt(yamcsStatus.value), 104 | label: yamcsStatus.label 105 | }; 106 | } 107 | toStatusFromTelemetry(telemetryObject, datum) { 108 | const metadata = this.#openmct.telemetry.getMetadata(telemetryObject); 109 | const rangeMetadata = metadata.valuesForHints(['range'])[0]; 110 | const formatter = this.#openmct.telemetry.getValueFormatter(rangeMetadata); 111 | const timestampMetadata = metadata.valuesForHints(['domain'])[0]; 112 | const dateFormatter = this.#openmct.telemetry.getValueFormatter(timestampMetadata); 113 | 114 | return { 115 | key: formatter.parse(datum), 116 | label: formatter.format(datum), 117 | timestamp: dateFormatter.parse(datum) 118 | }; 119 | 120 | } 121 | dictionaryLoadComplete() { 122 | this.#setReady(); 123 | } 124 | #buildUrl(id) { 125 | let url = `${this.#url}api/processors/${this.#instance}/${this.#processor}/parameters/${idToQualifiedName(id.key)}`; 126 | 127 | return url; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /.github/workflows/yamcs-quickstart-e2e.yml: -------------------------------------------------------------------------------- 1 | name: "yamcs-quickstart-e2e" 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | types: 6 | - labeled 7 | - opened 8 | schedule: 9 | - cron: "0 0 * * 1-5" 10 | push: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | yamcs-quickstart-e2e: 16 | if: ${{ github.event.label.name == 'pr:e2e:quickstart' }} || ${{ github.event.action == 'opened' }} 17 | timeout-minutes: 10 18 | runs-on: ubuntu-latest 19 | continue-on-error: false 20 | strategy: 21 | matrix: 22 | yamcs-version: 23 | - default 24 | - 5.9.8 #viper 25 | ## disabling until we get confirmation- 5.3.2 #ab 26 | openmct-version: 27 | - latest 28 | - stable 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: "18" 34 | - uses: actions/checkout@v3 35 | with: 36 | repository: yamcs/quickstart 37 | path: 'quickstart' 38 | - name: replace yamcsVersion with matrix param 39 | if: matrix.yamcs-version != 'default' 40 | run: | 41 | mvn -B versions:set-property -Dproperty=yamcsVersion -DnewVersion=${{ matrix.yamcs-version }} 42 | working-directory: quickstart 43 | - name: trigger make all from docker dir. Orphan and redirect output to docker/makeout.txt 44 | run: | 45 | touch makeout${{ matrix.yamcs-version }}-${{ matrix.openmct-version }}.txt 46 | make all-10hz &> makeout${{ matrix.yamcs-version }}-${{ matrix.openmct-version }}.txt & 47 | working-directory: quickstart/docker 48 | - run: npm install 49 | - name: Get Open MCT e2e tests 50 | uses: nick-fields/retry@v3 51 | with: 52 | timeout_minutes: 10 53 | max_attempts: 3 54 | command: npm run test:getopensource 55 | - name: Run build:example based on openmct-version 56 | run: | 57 | if [ "${{ matrix.openmct-version }}" = "latest" ]; then 58 | npm run build:example:master 59 | elif [ "${{ matrix.openmct-version }}" = "stable" ]; then 60 | npm run build:example 61 | fi 62 | - run: npx playwright@1.48.1 install chromium 63 | - name: Check that yamcs is available 64 | run: | 65 | docker ps -a 66 | npm run wait-for-yamcs 67 | - name: Check Yamcs installed version 68 | run: | 69 | response=$(curl -s -w "%{http_code}" -o yamcs_version.json http://localhost:8090/api/) 70 | if [ "$response" -eq 200 ]; then 71 | cat yamcs_version.json | jq '.yamcsVersion' 72 | else 73 | echo "Error: Unable to fetch Yamcs version. HTTP status code: $response" 74 | exit 1 75 | fi 76 | - name: Run Quickstart tests 77 | run: npm run test:e2e:quickstart 78 | - name: Capture docker logs to file 79 | if: always() 80 | run: docker logs yamcs > yamcs-docker-log-${{ matrix.yamcs-version }}-${{ matrix.openmct-version }}.txt 2>&1 81 | - name: archive docker logs 82 | if: always() 83 | uses: actions/upload-artifact@v4 84 | with: 85 | path: yamcs-docker-log-${{ matrix.yamcs-version }}-${{ matrix.openmct-version }}.txt 86 | name: yamcs-docker-log-${{ matrix.yamcs-version }}-${{ matrix.openmct-version }} 87 | - name: Archive makeout.txt 88 | if: always() 89 | uses: actions/upload-artifact@v4 90 | with: 91 | path: quickstart/docker/makeout${{ matrix.yamcs-version }}-${{ matrix.openmct-version }}.txt 92 | name: makeout-${{ matrix.yamcs-version }}-${{ matrix.openmct-version }} 93 | - name: Archive test results 94 | if: always() 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: test-results 98 | path: test-results 99 | name: test-results-${{ matrix.yamcs-version }}-${{ matrix.openmct-version }} 100 | - name: Archive html test results 101 | if: always() 102 | uses: actions/upload-artifact@v4 103 | with: 104 | name: html-test-results 105 | path: tests/html-test-results 106 | name: html-test-results-${{ matrix.yamcs-version }}-${{ matrix.openmct-version }} 107 | - name: Remove pr:e2e:quickstart label (if present) 108 | if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:quickstart') }} 109 | uses: actions/github-script@v6 110 | with: 111 | script: | 112 | const { owner, repo, number } = context.issue; 113 | const labelToRemove = 'pr:e2e:quickstart'; 114 | try { 115 | await github.rest.issues.removeLabel({ 116 | owner, 117 | repo, 118 | issue_number: number, 119 | name: labelToRemove 120 | }); 121 | } catch (error) { 122 | core.warning(`Failed to remove 'pr:e2e:quickstart' label: ${error.message}`); 123 | } 124 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/timeline.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import { pluginFixtures, appActions } from 'openmct-e2e'; 24 | import { postAllEvents } from '../../../example/make-example-events.mjs'; // Updated path and extension 25 | const { test, expect } = pluginFixtures; 26 | const { createDomainObjectWithDefaults, setStartOffset, setEndOffset, setFixedTimeMode } = appActions; 27 | 28 | test.describe("Timeline Events in @yamcs", () => { 29 | test('Can create a timeline with YAMCS events', async ({ page }) => { 30 | // Go to baseURL 31 | await page.goto("./", { waitUntil: "networkidle" }); 32 | await page.getByLabel('Expand myproject folder').click(); 33 | const eventsTreeItem = page.getByRole('treeitem', { name: /Events/ }); 34 | const eventTimelineView = await createDomainObjectWithDefaults(page, { type: 'Time Strip' }); 35 | const objectPane = page.getByLabel(`${eventTimelineView.name} Object View`); 36 | await eventsTreeItem.dragTo(objectPane); 37 | await postAllEvents(); 38 | 39 | await setStartOffset(page, { startMins: '02' }); 40 | await setEndOffset(page, { endMins: '02' }); 41 | await setFixedTimeMode(page); 42 | 43 | await page 44 | .getByLabel(eventTimelineView.name) 45 | .getByLabel(/Pressure threshold exceeded/) 46 | .first() 47 | .click(); 48 | await page.getByRole('tab', { name: 'Event' }).click(); 49 | 50 | // ensure the event inspector has the the same event 51 | await expect(page.getByText(/Pressure threshold exceeded/)).toBeVisible(); 52 | 53 | await page.getByLabel('Expand Events yamcs.events').click(); 54 | await page.getByLabel('Expand PressureModule yamcs.').click(); 55 | const pressureModuleInfoTreeItem = page.getByRole('treeitem', { name: /PressureModule: info/ }); 56 | await pressureModuleInfoTreeItem.dragTo(objectPane); 57 | 58 | const pressureModuleCriticalTreeItem = page.getByRole('treeitem', { name: /PressureModule: critical/ }); 59 | await pressureModuleCriticalTreeItem.dragTo(objectPane); 60 | 61 | // click on the event inspector tab 62 | await page.getByRole('tab', { name: 'Event' }).click(); 63 | 64 | await expect(page.getByLabel('PressureModule: info Object').getByLabel(/Pressure system check completed/).first()).toBeVisible(); 65 | await page.getByLabel('PressureModule: info Object').getByLabel(/Pressure system check completed/).first().click(); 66 | // ensure the tooltip shows up 67 | await expect( 68 | page.getByRole('tooltip').getByText(/Pressure system check completed/) 69 | ).toBeVisible(); 70 | 71 | // and that event appears in the inspector 72 | await expect( 73 | page.getByLabel('Inspector Views').getByText(/Pressure system check completed/) 74 | ).toBeVisible(); 75 | 76 | // info statements should be hidden in critical severity 77 | await expect(page.getByLabel('PressureModule: critical Object View').getByLabel(/Pressure system check/).first()).toBeHidden(); 78 | await expect(page.getByLabel('PressureModule: critical Object View').getByLabel(/Pressure threshold exceeded/).first()).toBeVisible(); 79 | await page.getByLabel('PressureModule: critical Object View').getByLabel(/Pressure threshold exceeded/).first().click(); 80 | await expect(page.getByLabel('Inspector Views').getByText('Pressure threshold exceeded')).toBeVisible(); 81 | await expect( 82 | page.getByRole('tooltip').getByText(/Pressure threshold exceeded/) 83 | ).toBeVisible(); 84 | 85 | // turn on extended lines 86 | await page.getByLabel('Toggle extended event lines overlay for PressureModule: critical').click(); 87 | const overlayLinesContainer = page.locator('.c-timeline__overlay-lines'); 88 | await expect(overlayLinesContainer.locator('.c-timeline__event-line--extended').last()).toBeVisible(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/actions/exportToCSV/ExportToCSVAction.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2023, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | import { OBJECT_TYPES } from "../../const.js"; 23 | import {idToQualifiedName} from "../../utils.js"; 24 | import { saveAs } from 'file-saver'; 25 | 26 | const SUPPORTED_TYPES = [OBJECT_TYPES.TELEMETRY_OBJECT_TYPE, OBJECT_TYPES.AGGREGATE_TELEMETRY_TYPE]; 27 | 28 | export default class ExportToCSVAction { 29 | constructor(openmct, url, instance) { 30 | this.name = 'Export to CSV'; 31 | this.key = 'exportToCSV'; 32 | this.cssClass = "icon-export"; 33 | this.description = 'Export the values for this telemetry object for a given time range.'; 34 | this.group = 'action'; 35 | this.priority = 2; 36 | 37 | this.url = url; 38 | this.instance = instance; 39 | this.openmct = openmct; 40 | } 41 | 42 | // Only allow export to CSV for telemetry objects, aggregates and objects with composition. 43 | // Objects with composition may not have valid children, which could result in an error. 44 | appliesTo(objectPath) { 45 | if (!objectPath.length) { 46 | return false; 47 | } 48 | 49 | const object = objectPath[0]; 50 | const composition = this.openmct.composition.get(object); 51 | 52 | //allow if the object has composition or if it is a supported telemetry object 53 | return composition !== undefined || SUPPORTED_TYPES.includes(object.type); 54 | } 55 | 56 | // Exports telemetry (or groups of telemetry) data into CSV file 57 | invoke(objectPath) { 58 | const object = objectPath[0]; 59 | let parameterIds = []; 60 | let parameterIdsPromise; 61 | const composition = this.openmct.composition.get(object); 62 | 63 | if (composition) { 64 | parameterIdsPromise = Promise.resolve(composition.load()); 65 | } else { 66 | parameterIdsPromise = Promise.resolve([object]); 67 | } 68 | 69 | parameterIdsPromise.then(async (childObjects) => { 70 | //Check if the objects are of a supported type 71 | childObjects.forEach(childObject => { 72 | if (SUPPORTED_TYPES.includes(childObject.type)) { 73 | parameterIds.push(this.getParameter(childObject.identifier)); 74 | } 75 | }); 76 | 77 | if (!parameterIds.length) { 78 | this.openmct.notifications.error(`Failed to export: no telemetry objects found`); 79 | 80 | return; 81 | } 82 | 83 | //Get the CSV data if some parameter ids are valid 84 | const response = await this.fetchExportedValues(parameterIds); 85 | if (response?.msg) { 86 | this.openmct.notifications.error(`Failed to export: ${response.msg}`); 87 | } else { 88 | let filename = `${parameterIds.join('-')}.csv`; 89 | let blob = new Blob([response], { type: "text/csv" }); 90 | saveAs(blob, filename); 91 | } 92 | }) 93 | .catch((error) => { 94 | this.openmct.notifications.error(`Failed to export: ${error}`); 95 | }); 96 | } 97 | 98 | //Change openmct id to yamcs name and remove namespace from identifier 99 | getParameter(identifier) { 100 | let id = this.openmct.objects.makeKeyString(identifier); 101 | id = idToQualifiedName(id).replace(/^.*:\//, ''); 102 | 103 | return id; 104 | } 105 | 106 | async fetchExportedValues(parameterIds) { 107 | const bounds = this.openmct.time.bounds(); 108 | const start = bounds.start; 109 | const end = bounds.end; 110 | const parameterIdsString = parameterIds.join(','); 111 | 112 | let url = `${this.url}api/archive/${this.instance}:exportParameterValues`; 113 | url += `?start=${new Date(start).toISOString()}`; 114 | url += `&stop=${new Date(end).toISOString()}`; 115 | url += `¶meters=${parameterIdsString}`; 116 | url += `&delimiter=COMMA`; 117 | 118 | const response = await fetch(url); 119 | if (!response.ok) { 120 | return response.json(); 121 | } else { 122 | return response.text(); 123 | } 124 | 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YAMCS Plugin for Open MCT 2 | This project provides a plugin for connecting Open MCT to a YAMCS backend. 3 | 4 | ## Running the example 5 | 6 | An example is provided in this repository that can be configured to run against any YAMCS installation. This is designed 7 | to get you up and running quickly, and should work with no configuration changes if you are running the 8 | [YAMCS QuickStart](https://github.com/yamcs/quickstart) server. 9 | 10 | ### Prerequisites 11 | * The YAMCS Quickstart example is assumed to have been installed and to be running successfully. See 12 | https://github.com/yamcs/quickstart 13 | * [A git client](https://git-scm.com/) 14 | * [NodeJS](https://nodejs.org/) 15 | 16 | ### Compatibility 17 | This is a fast moving project and we do our best to support and test what we test and support. 18 | 19 | * Supported NodeJS available in our package.json's `engine` key. 20 | * Minimum Supported Open MCT version in our package.json's `optionalDependencies` key. 21 | * Minimum YAMCS Version support follows [YAMCS QuickStart](https://github.com/yamcs/quickstart) 22 | 23 | If you encounter an issue with our documented compatibility, please file a [GitHub issue](https://github.com/akhenry/openmct-yamcs/issues/new/choose) 24 | 25 | ### Installation 26 | ``` 27 | git clone https://github.com/akhenry/openmct-yamcs.git 28 | cd openmct-yamcs 29 | npm install 30 | npm run build:example 31 | npm start 32 | ``` 33 | 34 | This should build the example, and launch a web browser with Open MCT connected to a locally running YAMCS server. By 35 | default it is configured to connect to the "myproject" instance provided in the [YAMCS QuickStart](https://github.com/yamcs/quickstart) server. 36 | 37 | > #### IMPORTANT NOTE 38 | > If Open MCT version issues are encountered with `npm run build:example`, try using `npm run build:example:master` to force usage of the latest version of Open MCT. 39 | 40 | ### Testing 41 | 42 | This project is using the openmct-e2e-as-a-dependency model. For getting started with our tests, please see [our README](./tests/README.md) 43 | 44 | Each PR is tested for compatibility with YAMCS QuickStart as well as the latest version of Open MCT using GitHub Actions. 45 | 46 | ## Using the Open MCT-YAMCS plugin in your own project 47 | 48 | When building an application with Open MCT, we strongly advise building with Open MCT as a dependency, rather than 49 | building your project from the Open MCT source. Please refer to 50 | [our guidance on this](https://github.com/nasa/openmct/blob/master/API.md#starting-an-open-mct-application). 51 | 52 | ### Fetching the dependency 53 | These instructions assume you are using Node Package Manager. You should be able to adapt them for Yarn, but this has 54 | not been tested. 55 | 56 | ``` 57 | npm install --save akhenry/openmct-yamcs 58 | ``` 59 | 60 | ### Installing the plugin 61 | 62 | The Open MCT - YAMCS adapter can be included as an ES6 module using an import statement. If you are using Webpack it 63 | can be imported using only the package name, otherwise the full path to the dependency should be used. 64 | 65 | eg. 66 | 67 | #### Using Webpack 68 | ``` 69 | import installYamcsPlugin from 'openmct-yamcs'; 70 | ``` 71 | 72 | #### Using native ES6 imports: 73 | ``` 74 | import installYamcsPlugin from 'node_modules/openmct-yamcs/dist/openmct-yamcs.js' 75 | ``` 76 | 77 | The plugin can then be installed and configured like so: 78 | ``` 79 | openmct.install(installYamcsPlugin({ 80 | "yamcsDictionaryEndpoint": "http://localhost:8090/", 81 | "yamcsHistoricalEndpoint": "http://localhost:8090/", 82 | "yamcsWebsocketEndpoint": "ws://localhost:8090/", 83 | "yamcsInstance": "myproject", 84 | "yamcsFolder": "myproject" 85 | })); 86 | ``` 87 | 88 | ## Configuration 89 | | Configuration Item | Notes | Example Value | 90 | |-------------------------|-------------------------------------------------------|------------------------------------| 91 | | yamcsDictionaryEndpoint | This is the root path to the YAMCS installation. The adapter will use this to fetch all of the parameters adapter will use this to fetch all of the parameters and containers defined for the configured instance. | http://localhost:8090/ | 92 | | yamcsHistoricalEndpoint | As above, this is the root path to the YAMCS installation. This will be automatically appended with the necessary path to retrieve historical data for the selected parameter, in the configured instance. | http://localhost:8090/ | 93 | | yamcsWebsocketEndpoint | The path to the new (post v5) WebSocket interface. *It must always start with `ws` or `wss`, and must contain the complete path (unlike config above) | ws://localhost:8090/api/websocket | 94 | | yamcsInstance | The name of the instance configured in YAMCS that you wish to connect to. | myproject | 95 | | yamcsFolder | The name of the instance configured in YAMCS that you wish to connect to. | myproject | 96 | 97 | ## getDictionaryRequestOptions 98 | installYamcsPlugin also accepts an optional function argument `getDictionaryRequestOptions`. Use this function to return request options when requesting the YAMCS dictionary. An example of how to make use of this is below. 99 | ``` 100 | openmct.install(installYamcsPlugin( 101 | configuration, 102 | getDictionaryRequestOptions 103 | )) 104 | 105 | function getDictionaryRequestOptions() { 106 | const requestOptions = SOME_CHECK_IF_DICTIONARY_VERSION_IS_NEW 107 | ? { cache: 'reload' } 108 | : {}; 109 | 110 | return requestOptions; 111 | } 112 | ``` 113 | 114 | ## Special XTCE features 115 | 116 | If you are using an XTCE configuration in Yamcs, there are two special 117 | constructs you can use in the XTCE to control how a parameter is used in 118 | Yamcs. Both are specified in an `` associated with a parameter 119 | definition. The `namespace` attribute of the `` is one of these 120 | two values: 121 | 122 | * `OpenMCT:type` -- This namespace indicates that the parameter type in OpenMCT should be taken from the `alias` attribute rather than inferred from the Yamcs type. The main use of this construct is to mark string parameters which should be interpreted as image URLs. Those should have a `alias` attribute of `yamcs.image`. 123 | * `OpenMCT:omit` -- This namespace indicates that the parameter should be omitted from the OpenMCT object tree. This is useful for parameters that are used as filler values, or for packet values that are always constant. Using this construct you can declutter the OpenMCT object tree. 124 | 125 | Examples: 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | ## Limitations 141 | Right now the Open MCT - YAMCS adapter can only be configued for a single YAMCS instance at a time. We hope to address this in a future release. 142 | -------------------------------------------------------------------------------- /src/providers/events.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | import { OBJECT_TYPES, METADATA_TIME_KEY, SEVERITY_LEVELS } from "../const.js"; 23 | 24 | export function createRootEventsObject(openmct, parentKey, namespace) { 25 | const rootEventIdentifier = { 26 | key: OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE, 27 | namespace 28 | }; 29 | const rootEventObject = createEventObject(openmct, parentKey, namespace, rootEventIdentifier); 30 | rootEventObject.composition = []; 31 | 32 | return rootEventObject; 33 | } 34 | 35 | export function createEventObject(openmct, parentKey, namespace, identifier, name = 'Events', type = OBJECT_TYPES.EVENTS_ROOT_OBJECT_TYPE) { 36 | const location = openmct.objects.makeKeyString({ 37 | key: parentKey, 38 | namespace 39 | }); 40 | 41 | const baseEventObject = { 42 | identifier, 43 | location, 44 | name, 45 | type, 46 | telemetry: { 47 | values: [ 48 | { 49 | key: 'severity', 50 | name: 'Severity Threshold', 51 | format: 'string', 52 | filters: [{ 53 | singleSelectionThreshold: true, 54 | comparator: 'equals', 55 | possibleValues: SEVERITY_LEVELS.map((level) => { 56 | return { 57 | label: level, 58 | value: level 59 | }; 60 | }) 61 | }] 62 | }, 63 | { 64 | key: 'utc', 65 | source: METADATA_TIME_KEY, 66 | name: 'Generation Time', 67 | format: 'iso', 68 | hints: { 69 | domain: 1 70 | } 71 | }, 72 | { 73 | key: 'receptionTime', 74 | name: 'Reception Time', 75 | format: 'iso' 76 | }, 77 | { 78 | key: 'seqNumber', 79 | name: 'Sequence Number', 80 | format: 'number' 81 | }, 82 | { 83 | key: 'message', 84 | name: 'Message', 85 | format: 'string', 86 | hints: { 87 | // this is used in the EventTimelineView to provide a title for the event 88 | // label can be changed to other properties for the title (e.g., the `name` property) 89 | label: 0 90 | } 91 | }, 92 | { 93 | key: 'type', 94 | name: 'Type', 95 | format: 'string' 96 | }, 97 | { 98 | key: 'source', 99 | name: 'Source', 100 | format: 'string' 101 | }, 102 | { 103 | key: 'createdBy', 104 | name: 'Created By', 105 | format: 'string' 106 | } 107 | ] 108 | } 109 | }; 110 | 111 | return baseEventObject; 112 | } 113 | 114 | export function createEventSeverityObjects(openmct, parentEventObject, namespace) { 115 | const childSeverityObjects = []; 116 | for (const severity of SEVERITY_LEVELS) { 117 | const severityIdentifier = { 118 | key: `${parentEventObject.identifier.key}.${severity}`, 119 | namespace 120 | }; 121 | 122 | const severityName = `${parentEventObject.name}: ${severity}`; 123 | 124 | const severityEventObject = createEventObject( 125 | openmct, 126 | parentEventObject.identifier.key, 127 | namespace, 128 | severityIdentifier, 129 | severityName, 130 | OBJECT_TYPES.EVENT_SPECIFIC_SEVERITY_OBJECT_TYPE 131 | ); 132 | 133 | childSeverityObjects.push(severityEventObject); 134 | } 135 | 136 | return childSeverityObjects; 137 | } 138 | 139 | export async function getEventSources(url, instance) { 140 | const eventSourceURL = `${url}api/archive/${instance}/events/sources`; 141 | const eventSourcesReply = await fetch(eventSourceURL); 142 | if (!eventSourcesReply.ok) { 143 | console.error(`🛑 Failed to fetch event sources: ${eventSourcesReply.statusText}`); 144 | 145 | return []; 146 | } 147 | 148 | const eventSourcesJson = await eventSourcesReply.json(); 149 | 150 | if (eventSourcesJson.sources) { 151 | return eventSourcesJson.sources; 152 | } else if (eventSourcesJson.source) { 153 | // backwards compatibility with older YAMCS versions that only have `source` key 154 | return eventSourcesJson.source; 155 | } else { 156 | return []; 157 | } 158 | } 159 | 160 | export function eventShouldBeFiltered(event, options) { 161 | const { severity } = event; 162 | const incomingEventSeverity = severity?.toLowerCase(); 163 | const severityLevelToFilter = options?.filters?.severity?.equals?.[0]; 164 | const severityLevelToFilterIndex = SEVERITY_LEVELS.indexOf(severityLevelToFilter); 165 | const incomingEventSeverityIndex = SEVERITY_LEVELS.indexOf(incomingEventSeverity); 166 | 167 | return incomingEventSeverityIndex < severityLevelToFilterIndex; 168 | } 169 | 170 | /** 171 | * Convert raw event data from YAMCS to a format which 172 | * can be consumed by Open MCT as telemetry. 173 | * @param {Object} command 174 | * @returns {Object} telemetryDatum 175 | */ 176 | export function eventToTelemetryDatum(event) { 177 | const { 178 | severity, 179 | generationTime, 180 | receptionTime, 181 | seqNumber, 182 | message, 183 | type, 184 | source, 185 | createdBy 186 | } = event; 187 | 188 | return { 189 | id: OBJECT_TYPES.EVENT_SPECIFIC_OBJECT_TYPE, 190 | severity, 191 | generationTime, 192 | receptionTime, 193 | seqNumber, 194 | message, 195 | type, 196 | source, 197 | createdBy 198 | }; 199 | } 200 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/telemetryTables.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | Telemetry Table Specific Tests 25 | */ 26 | 27 | import { pluginFixtures, appActions } from 'openmct-e2e'; 28 | const { test, expect } = pluginFixtures; 29 | const { setRealTimeMode } = appActions; 30 | const FIVE_SECONDS = 5 * 1000; 31 | 32 | test.describe("Telemetry Tables tests @yamcs", () => { 33 | 34 | // An error will be thrown if an attempt to mutate an immutable object is made, this will cover 35 | // that case as well as any other errors during the test 36 | test.use({ failOnConsoleError: true }); 37 | 38 | test.beforeEach(async ({ page }) => { 39 | // Go to baseURL 40 | await page.goto("./", { waitUntil: "domcontentloaded" }); 41 | await expect(page.getByText('Loading...')).toBeHidden(); 42 | }); 43 | 44 | test('Telemetry Tables viewing an unpersistable object, will not modify the configuration on mode change', async ({ page }) => { 45 | // Navigate to the Events table 46 | await page.goto('./#/browse/taxonomy:spacecraft/taxonomy:yamcs.events', { waitUntil: 'networkidle' }); 47 | 48 | // Find the mode switch button and click it, this will trigger a mutation on mutable objects configuration 49 | await page.getByRole('button', { name: 'SHOW UNLIMITED' }).click(); 50 | 51 | // Assert that the 'SHOW LIMITED' button is now visible 52 | await expect(page.getByRole('button', { name: 'SHOW LIMITED' })).toBeVisible(); 53 | }); 54 | 55 | test('Telemetry tables when changing mode, will not change the sort order of the request', async ({ page }) => { 56 | // Set up request promise for an events request in descending order 57 | let eventRequestOrderDescending = page.waitForRequest(/.*\/api\/.*\/events.*order=desc$/); 58 | 59 | // Navigate to the Events table 60 | await page.goto('./#/browse/taxonomy:spacecraft/taxonomy:yamcs.events', { waitUntil: 'networkidle' }); 61 | 62 | // Wait for the descending events request 63 | await eventRequestOrderDescending; 64 | 65 | // Reset request promise for an events request in descending order 66 | eventRequestOrderDescending = page.waitForRequest(/.*\/api\/.*\/events.*order=desc$/); 67 | 68 | // Find the mode switch button and click it, this will trigger another events request 69 | await page.getByRole('button', { name: 'SHOW UNLIMITED' }).click(); 70 | await page.waitForLoadState('networkidle'); 71 | 72 | await eventRequestOrderDescending; 73 | 74 | // Assert that the 'SHOW LIMITED' button is now visible 75 | await expect(page.getByRole('button', { name: 'SHOW LIMITED' })).toBeVisible(); 76 | }); 77 | 78 | test('Changing sort order in limited mode triggers a new request', async ({ page }) => { 79 | // Set up request promise for an events request in descending order 80 | const eventRequestOrderDescending = page.waitForRequest(/.*\/api\/.*\/events.*order=desc$/); 81 | 82 | // Navigate to the Events table 83 | await page.goto('./#/browse/taxonomy:spacecraft/taxonomy:yamcs.events', { waitUntil: 'networkidle' }); 84 | 85 | // Wait for and verify that the request was made 86 | await expect(eventRequestOrderDescending).resolves.toBeTruthy(); 87 | 88 | // Assert that the 'SHOW UNLIMITED' button is visible (we are in limited mode) 89 | await expect(page.getByRole('button', { name: 'SHOW UNLIMITED' })).toBeVisible(); 90 | 91 | // Set up request promise before clicking to change sort order 92 | const eventRequestOrderAscending = page.waitForRequest(/.*\/api\/.*\/events.*order=asc$/); 93 | 94 | // flip sort order 95 | await page.locator('thead div').filter({ hasText: 'Generation Time' }).click(); 96 | 97 | // Wait for and verify that the request was made 98 | await expect(eventRequestOrderAscending).resolves.toBeTruthy(); 99 | }); 100 | 101 | test('Telemetry tables are sorted in desc order correctly', async ({ page }) => { 102 | await setRealTimeMode(page); 103 | 104 | //navigate to CCSDS_Packet_Length with a specified realtime window 105 | await page.goto('./#/browse/taxonomy:spacecraft/taxonomy:~myproject/taxonomy:~myproject~CCSDS_Packet_Length?tc.mode=local&tc.startDelta=5000&tc.endDelta=5000&tc.timeSystem=utc&view=table', {waitUntil: 'domcontentloaded'}); 106 | 107 | // wait 5 seconds for the table to fill 108 | await page.waitForTimeout(FIVE_SECONDS); 109 | // pause the table 110 | await page.getByLabel('Pause').click(); 111 | const telemTableDesc = await page.getByLabel("CCSDS_Packet_Length table content"); 112 | 113 | // assert that they're in desc order 114 | expect(await assertTableRowsInOrder(telemTableDesc, 'desc')).toBe(true); 115 | 116 | // Unpause 117 | await page.getByLabel('Play').click(); 118 | 119 | // flip sort order 120 | await page.locator('thead div').filter({ hasText: 'Timestamp' }).click(); 121 | 122 | // wait for x seconds 123 | await page.waitForTimeout(FIVE_SECONDS); 124 | 125 | // pause the table 126 | await page.getByLabel('Pause').click(); 127 | const telemTableAsc = await page.getByLabel("CCSDS_Packet_Length table content"); 128 | // assert that they're in asc order 129 | expect(await assertTableRowsInOrder(telemTableAsc, 'asc')).toBe(true); 130 | }); 131 | 132 | /** 133 | * Returns whether a list of timestamp based rows are in asc or desc order 134 | * @param { Node } telemTable Node for telemetry table 135 | * @param { string } order 'asc' or 'desc' 136 | * @returns {Boolean} All table rows are in order 137 | */ 138 | async function assertTableRowsInOrder(telemTable, order) { 139 | let rowsAreInOrder = false; 140 | const allRows = await (await telemTable.getByLabel('Table Row')).all(); 141 | const arrayOfTimestamps = await Promise.all(allRows.map(async (row) => { 142 | const timestamp = await row.getByLabel(/utc table cell.*/).innerText(); 143 | 144 | return new Date(timestamp).getTime(); 145 | })); 146 | // check that they're in order 147 | // arrayOfTimestamps 148 | if (order === 'desc') { 149 | rowsAreInOrder = arrayOfTimestamps.every((timestamp, index) => { 150 | return index === 0 || timestamp <= arrayOfTimestamps[index - 1]; 151 | }); 152 | } else { 153 | //order === 'asc' 154 | rowsAreInOrder = arrayOfTimestamps.every((timestamp, index) => { 155 | return index === 0 || timestamp >= arrayOfTimestamps[index - 1]; 156 | }); 157 | } 158 | 159 | return rowsAreInOrder; 160 | } 161 | 162 | }); 163 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/network.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | /* 23 | * Network Specific Tests for Open MCT and YAMCS connectivity. 24 | * This suite verifies the network requests made by the application to ensure correct interaction with YAMCS. 25 | */ 26 | 27 | import { pluginFixtures, appActions } from 'openmct-e2e'; 28 | const { test, expect } = pluginFixtures; 29 | const { setFixedTimeMode } = appActions; 30 | 31 | /** 32 | * This test suite checks the network requests made by Open MCT to YAMCS. 33 | */ 34 | test.describe("Quickstart network requests @yamcs", () => { 35 | // Keeping track of network requests during the tests. 36 | let networkRequests = []; 37 | let filteredRequests = []; 38 | 39 | // These variables hold the promises for specific network requests we expect to occur. 40 | let parameterArchiveGet, batchGetStaleness, allParams, userGet, mdbOverride, mdbGet; 41 | 42 | test('Validate network traffic to YAMCS', async ({ page }) => { 43 | // Listening for all network requests and pushing them into networkRequests array. 44 | page.on('request', request => networkRequests.push(request)); 45 | 46 | // Setting up promises to wait for specific network responses. 47 | networkRequests = []; 48 | mdbGet = page.waitForResponse('**/api/mdb/myproject/space-systems'); 49 | allParams = page.waitForResponse('**/api/mdb/myproject/parameters?details=yes&limit=1000'); 50 | userGet = page.waitForResponse('**/api/user/'); 51 | mdbOverride = page.waitForResponse('**/api/mdb-overrides/myproject/realtime'); 52 | 53 | // Testing the initial page load and verifying the presence of specific elements. 54 | await page.goto("./", { waitUntil: "networkidle" }); 55 | await Promise.all([mdbGet, allParams, userGet, mdbOverride]); 56 | filteredRequests = filterNonFetchRequests(networkRequests); 57 | expect(filteredRequests.length).toBe(4); 58 | 59 | // I'm not sure what is going on here 60 | const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject' }); 61 | await expect(myProjectTreeItem).toBeVisible(); 62 | await myProjectTreeItem.first().locator('span.c-disclosure-triangle').click(); 63 | await myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle').click(); 64 | 65 | // More UI interactions and network request verifications. 66 | await page.waitForLoadState('networkidle'); 67 | networkRequests = []; 68 | batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); 69 | await page.getByRole('treeitem', { name: 'Expand CCSDS_Packet_Sequence' }).click(); 70 | await batchGetStaleness; 71 | 72 | await page.waitForLoadState('networkidle'); 73 | expect(networkRequests.length).toBe(1); 74 | 75 | // Further UI interactions and network requests verification. 76 | networkRequests = []; 77 | parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); 78 | batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); 79 | await page.getByRole('treeitem', { name: 'CCSDS_Packet_Length' }).click(); 80 | 81 | await Promise.all([parameterArchiveGet, batchGetStaleness]); 82 | 83 | await page.waitForLoadState('networkidle'); 84 | expect(networkRequests.length).toBe(2); 85 | 86 | // Simulating the change to fixed time mode and validating network requests. 87 | networkRequests = []; 88 | parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); 89 | await setFixedTimeMode(page); 90 | await page.waitForLoadState('networkidle'); 91 | await parameterArchiveGet; 92 | expect(networkRequests.length).toBe(1); 93 | 94 | // Clicking on a different telemetry item to generate new requests. 95 | networkRequests = []; 96 | let groupFlagsGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Sequence.GroupFlags**'); 97 | let countGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Sequence.Count**'); 98 | batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); 99 | 100 | await page.getByRole('treeitem', { name: 'Expand CCSDS_Packet_Sequence' }).click(); 101 | await page.waitForLoadState('networkidle'); 102 | 103 | await Promise.all([groupFlagsGet, countGet, batchGetStaleness]); 104 | 105 | expect(networkRequests.length).toBe(3); 106 | 107 | // Clicking on the telemetry item in Fixed Time mode to generate two requests. 108 | networkRequests = []; 109 | parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); 110 | batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); 111 | await page.getByRole('treeitem', { name: 'CCSDS_Packet_Length' }).click(); 112 | await page.waitForLoadState('networkidle'); 113 | 114 | await Promise.all([parameterArchiveGet, batchGetStaleness]); 115 | 116 | // Waiting for debounced requests in YAMCS Latest Telemetry Provider to finish. 117 | expect(networkRequests.length).toBe(2); 118 | 119 | // Simulating a page refresh to generate a sequence of network requests. 120 | networkRequests = []; 121 | userGet = page.waitForResponse('**/api/user/'); 122 | allParams = page.waitForResponse('**/api/mdb/myproject/parameters?details=yes&limit=1000'); 123 | mdbOverride = page.waitForResponse('**/api/mdb-overrides/myproject/realtime'); 124 | parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); 125 | batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); 126 | mdbOverride = page.waitForResponse('**/api/mdb-overrides/myproject/realtime'); 127 | 128 | await page.reload({ waitUntil: 'networkidle' }); 129 | await Promise.all([allParams, userGet, mdbOverride, parameterArchiveGet, batchGetStaleness, mdbOverride]); 130 | 131 | // Waiting for debounced requests in YAMCS Latest Telemetry Provider to finish. 132 | filteredRequests = filterNonFetchRequests(networkRequests); 133 | expect(filteredRequests.length).toBe(6); 134 | 135 | // Removing the 'request' event listener to prevent potential memory leaks. 136 | page.removeListener('request', request => networkRequests.push(request)); 137 | }); 138 | 139 | /** 140 | * Filters out non-fetch requests from the given array of network requests. 141 | * This includes preflight CORS, fetching stylesheets, page icons, etc. 142 | * @param {Array} requests - Array of network requests to filter. 143 | * @returns {Array} Filtered network requests. 144 | */ 145 | function filterNonFetchRequests(requests) { 146 | return requests.filter(request => request.resourceType() === 'fetch'); 147 | } 148 | }); 149 | -------------------------------------------------------------------------------- /src/providers/commands.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | import { OBJECT_TYPES, METADATA_TIME_KEY } from "../const.js"; 24 | import { flattenObjectArray } from "../utils.js"; 25 | 26 | export function createRootCommandsObject(openmct, parentKey, namespace) { 27 | const rootCommandsIdentifier = { 28 | key: OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE, 29 | namespace 30 | }; 31 | const rootCommandsObject = createCommandObject(openmct, parentKey, namespace, rootCommandsIdentifier); 32 | 33 | return rootCommandsObject; 34 | } 35 | 36 | export function createCommandObject(openmct, parentKey, namespace, identifier, queueName = null) { 37 | const isRoot = queueName === null; 38 | const location = openmct.objects.makeKeyString({ 39 | key: parentKey, 40 | namespace 41 | }); 42 | 43 | const name = isRoot ? 'Commands' : queueName; 44 | const type = isRoot 45 | ? OBJECT_TYPES.COMMANDS_ROOT_OBJECT_TYPE 46 | : OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE; 47 | 48 | const commandObject = { 49 | identifier, 50 | location, 51 | name, 52 | type, 53 | telemetry: { 54 | values: [ 55 | { 56 | key: 'commandName', 57 | name: 'Command', 58 | format: 'string', 59 | hints: { 60 | // this is to ensure that the commandName is displayed as a label 61 | label: 0 62 | } 63 | }, 64 | { 65 | key: 'utc', 66 | source: METADATA_TIME_KEY, 67 | name: 'Generation Time', 68 | format: 'iso', 69 | hints: { 70 | domain: 1 71 | } 72 | }, 73 | { 74 | key: 'sequenceNumber', 75 | name: 'Sequence Number', 76 | format: 'number' 77 | }, 78 | { 79 | key: 'comment', 80 | name: 'Comment', 81 | format: 'string' 82 | }, 83 | { 84 | key: 'source', 85 | name: 'Source', 86 | format: 'string' 87 | }, 88 | { 89 | key: 'queue', 90 | name: 'Queue', 91 | format: 'string' 92 | }, 93 | { 94 | key: 'binary', 95 | name: 'Binary', 96 | format: 'binary' 97 | }, 98 | { 99 | key: 'unprocessedBinary', 100 | name: 'Unprocessed Binary', 101 | format: 'string' 102 | }, 103 | { 104 | key: 'Acknowledge_Queued_Status', 105 | name: 'Acknowledge Queued Status', 106 | format: 'string' 107 | }, 108 | { 109 | key: 'Acknowledge_Queued_Time', 110 | name: 'Acknowledge Queued Time', 111 | format: 'iso' 112 | }, 113 | { 114 | key: 'Acknowledge_Released_Status', 115 | name: 'Acknowledge Released Status', 116 | format: 'string' 117 | }, 118 | { 119 | key: 'Acknowledge_Released_Time', 120 | name: 'Acknowledge Released Time', 121 | format: 'iso' 122 | }, 123 | { 124 | key: 'Acknowledge_Sent_Status', 125 | name: 'Acknowledge Sent Status', 126 | format: 'string' 127 | }, 128 | { 129 | key: 'Acknowledge_Sent_Time', 130 | name: 'Acknowledge Sent Time', 131 | format: 'iso' 132 | }, 133 | { 134 | key: 'username', 135 | name: 'Issuer', 136 | format: 'string' 137 | }, 138 | { 139 | key: 'origin', 140 | name: 'Origin', 141 | format: 'string' 142 | }, 143 | { 144 | key: 'CCSDS_Version', 145 | name: 'CCSDS Version', 146 | format: 'string' 147 | }, 148 | { 149 | key: 'CCSDS_Type', 150 | name: 'CCSDS Type', 151 | format: 'string' 152 | }, 153 | { 154 | key: 'CCSDS_Sec_Hdr_Flag', 155 | name: 'CCSDS Sec Hdr Flag', 156 | format: 'string' 157 | }, 158 | { 159 | key: 'CCSDS_APID', 160 | name: 'CCSDS APID', 161 | format: 'string' 162 | }, 163 | { 164 | key: 'CCSDS_Group_Flags', 165 | name: 'CCSDS Group Flags', 166 | format: 'string' 167 | }, 168 | { 169 | key: 'Packet_ID', 170 | name: 'Packet ID', 171 | format: 'string' 172 | }, 173 | { 174 | key: 'messageId', 175 | name: 'row identifier', 176 | format: 'string', 177 | useToUpdateInPlace: true 178 | } 179 | ] 180 | } 181 | }; 182 | 183 | if (isRoot) { 184 | commandObject.composition = []; 185 | } 186 | 187 | return commandObject; 188 | } 189 | 190 | export async function getCommandQueues(url, instance, processor = 'realtime') { 191 | const commandQueuesURL = `${url}api/processors/${instance}/${processor}/queues`; 192 | const response = await fetch(commandQueuesURL, { 193 | method: 'GET', 194 | headers: { 195 | 'Content-Type': 'application/json' 196 | } 197 | }); 198 | 199 | if (!response.ok) { 200 | console.error(`🛑 Error fetching command queues: ${response.statusText}`); 201 | 202 | return []; 203 | } 204 | 205 | const commandQueueJson = await response.json(); 206 | const { queues } = commandQueueJson; 207 | const queueNames = queues.map(queue => queue.name); 208 | 209 | return queueNames; 210 | } 211 | 212 | /** 213 | * Convert raw command data from YAMCS to a format which 214 | * can be consumed by Open MCT as telemetry. 215 | * @param {Object} command 216 | * @returns {Object} telemetryDatum 217 | */ 218 | export function commandToTelemetryDatum(command) { 219 | const { generationTime, commandId, attr, assignments, id } = command; 220 | const { origin, sequenceNumber, commandName } = commandId; 221 | let datum = { 222 | id: OBJECT_TYPES.COMMANDS_QUEUE_OBJECT_TYPE, 223 | generationTime, 224 | origin, 225 | sequenceNumber, 226 | commandName, 227 | messageId: id 228 | }; 229 | datum = attr ? flattenObjectArray(attr, datum) : datum; 230 | datum = assignments ? flattenObjectArray(assignments, datum) : datum; 231 | 232 | return datum; 233 | } 234 | -------------------------------------------------------------------------------- /tests/e2e/yamcs/historicalData.e2e.spec.mjs: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2022, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | 23 | /* 24 | Network Specific Tests 25 | */ 26 | 27 | import { pluginFixtures, appActions } from 'openmct-e2e'; 28 | const { test, expect } = pluginFixtures; 29 | const { setFixedTimeMode } = appActions; 30 | 31 | test.describe("Samples endpoint with useRawValue search param @yamcs", () => { 32 | // Collect all request events, specifically for YAMCS 33 | let filteredRequests = []; 34 | let networkRequests = []; 35 | test.beforeEach(async ({ page }) => { 36 | page.on('request', (request) => networkRequests.push(request)); 37 | // Go to baseURL 38 | await page.goto("./", { waitUntil: "domcontentloaded" }); 39 | await expect(page.getByText('Loading...')).toBeHidden(); 40 | // Change to fixed time 41 | await setFixedTimeMode(page); 42 | 43 | // Expand myproject and subfolder myproject 44 | await page.getByLabel('Expand myproject').click(); 45 | await page.getByLabel('Expand myproject').click(); 46 | // await expect(page.getByText('Loading...')).toBeHidden(); 47 | networkRequests = []; 48 | filteredRequests = []; 49 | }); 50 | 51 | test('When in plot view, samples endpoint is used for enum type parameters with the useRawValue parameter', async ({ page }) => { 52 | await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); 53 | 54 | // wait for debounced requests in YAMCS Latest Telemetry Provider to finish 55 | // FIXME: can we use waitForRequest? 56 | await page.waitForTimeout(500); 57 | 58 | filteredRequests = filterNonFetchRequests(networkRequests); 59 | 60 | // 1. batched request for latest telemetry using the bulk API 61 | // 2. samples request for telemetry data 62 | const samplesRequests = filteredRequests.filter(request => request.url().indexOf('/samples') > -1); 63 | const nonSampleRequests = filteredRequests.filter(request => request.url().indexOf('/samples') < 0); 64 | expect(samplesRequests.length).toBe(1); 65 | expect(samplesRequests[0].url()).toContain("useRawValue=true"); 66 | expect(nonSampleRequests.length).toBe(1); 67 | }); 68 | 69 | test('When in plot view, samples endpoint is used for scalar (number) type parameters with no useRawValue parameter', async ({ page }) => { 70 | await page.getByLabel('Navigate to CCSDS_Packet_Length yamcs').click(); 71 | 72 | // wait for debounced requests in YAMCS Latest Telemetry Provider to finish 73 | // FIXME: can we use waitForRequest? 74 | await page.waitForTimeout(500); 75 | 76 | filteredRequests = filterNonFetchRequests(networkRequests); 77 | 78 | // 1. batched request for latest telemetry using the bulk API 79 | // 2. samples request for telemetry data 80 | const samplesRequests = filteredRequests.filter(request => request.url().indexOf('/samples') > -1); 81 | const nonSampleRequests = filteredRequests.filter(request => request.url().indexOf('/samples') < 0); 82 | expect(samplesRequests.length).toBe(1); 83 | expect(samplesRequests[0].url()).not.toContain("useRawValue"); 84 | expect(nonSampleRequests.length).toBe(1); 85 | }); 86 | 87 | test('When in table view, samples endpoint and useRawValue are not used for scalar (number) type parameters', async ({ page }) => { 88 | await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); 89 | 90 | //switch to table view 91 | networkRequests = []; 92 | await page.getByLabel('Open the View Switcher Menu').click(); 93 | await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); 94 | await page.waitForLoadState('networkidle'); 95 | 96 | // wait for debounced requests in YAMCS Latest Telemetry Provider to finish 97 | // FIXME: can we use waitForRequest? 98 | await page.waitForTimeout(500); 99 | 100 | filteredRequests = filterNonFetchRequests(networkRequests); 101 | 102 | // 1. batched request for latest telemetry using the bulk API 103 | // 2. samples request for telemetry data 104 | //Switch view to table 105 | // 3. batched request for latest telemetry using the bulk API 106 | // 4. parameters history request for telemetry data 107 | // 5. parameters history request for telemetry data with token 108 | const samplesRequests = filteredRequests.filter(request => request.url().indexOf('/samples') > -1); 109 | const nonSampleRequests = filteredRequests.filter(request => request.url().indexOf('/samples') < 0); 110 | expect(samplesRequests.length).toBe(0); 111 | expect(nonSampleRequests.length).toBe(filteredRequests.length); 112 | }); 113 | 114 | test('When in table view and in unlimited mode, requests contain the "order=desc" parameter', async ({ page }) => { 115 | await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); 116 | 117 | //switch to table view 118 | networkRequests = []; 119 | await page.getByLabel('Open the View Switcher Menu').click(); 120 | await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); 121 | await page.waitForLoadState('networkidle'); 122 | 123 | // wait for debounced requests in YAMCS Latest Telemetry Provider to finish 124 | // FIXME: can we use waitForRequest? 125 | await page.waitForTimeout(500); 126 | 127 | filteredRequests = filterNonFetchRequests(networkRequests); 128 | // Verify we are in "Limited" mode 129 | await expect(page.getByRole('button', { name: 'SHOW UNLIMITED' })).toBeVisible(); 130 | 131 | // Check if any request URL contains the 'order=desc' parameter 132 | const hasOrderDesc = filteredRequests.some(request => request.url().includes('order=desc')); 133 | expect(hasOrderDesc).toBe(true); 134 | }); 135 | 136 | test('When in table view, samples endpoint and useRawValue are not used for enum type parameters', async ({ page }) => { 137 | await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); 138 | 139 | //switch to table view 140 | networkRequests = []; 141 | await page.getByLabel('Open the View Switcher Menu').click(); 142 | await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); 143 | await page.waitForLoadState('networkidle'); 144 | 145 | // wait for debounced requests in YAMCS Latest Telemetry Provider to finish 146 | // FIXME: can we use waitForRequest? 147 | await page.waitForTimeout(500); 148 | 149 | filteredRequests = filterNonFetchRequests(networkRequests); 150 | 151 | // 1. batched request for latest telemetry using the bulk API 152 | // 2. samples request for telemetry data 153 | //Switch view to table 154 | // 3. batched request for latest telemetry using the bulk API 155 | // 4. parameters history request for telemetry data 156 | // 5. parameters history request for telemetry data with token 157 | const samplesRequests = filteredRequests.filter(request => request.url().indexOf('/samples') > -1); 158 | const nonSampleRequests = filteredRequests.filter(request => request.url().indexOf('/samples') < 0); 159 | expect(samplesRequests.length).toBe(0); 160 | expect(nonSampleRequests.length).toBe(filteredRequests.length); 161 | }); 162 | 163 | // Try to reduce indeterminism of browser requests by only returning fetch requests. 164 | // Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests 165 | function filterNonFetchRequests(requests) { 166 | return requests.filter(request => { 167 | return (request.resourceType() === 'fetch'); 168 | }); 169 | } 170 | }); 171 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import installYamcsPlugins from '../src/openmct-yamcs.js'; 2 | 3 | const config = { 4 | yamcsDictionaryEndpoint: "http://localhost:9000/yamcs-proxy/", 5 | yamcsHistoricalEndpoint: "http://localhost:9000/yamcs-proxy/", 6 | yamcsWebsocketEndpoint: "ws://localhost:9000/yamcs-proxy-ws/", 7 | yamcsUserEndpoint: "http://localhost:9000/yamcs-proxy/api/user/", 8 | yamcsInstance: "myproject", 9 | yamcsProcessor: "realtime", 10 | yamcsFolder: "myproject", 11 | throttleRate: 1000, 12 | // Batch size is specified in characers as there is no performant way of calculating true 13 | // memory usage of a string buffer in real-time. 14 | // String characters can be 8 or 16 bits in JavaScript, depending on the code page used. 15 | // Thus 500,000 characters requires up to 16MB of memory (1,000,000 * 16). 16 | maxBufferSize: 1000000 17 | }; 18 | const STATUS_STYLES = { 19 | NO_STATUS: { 20 | iconClass: "icon-question-mark", 21 | iconClassPoll: "icon-status-poll-question-mark" 22 | }, 23 | GO: { 24 | iconClass: "icon-check", 25 | iconClassPoll: "icon-status-poll-question-mark", 26 | statusClass: "s-status-ok", 27 | statusBgColor: "#33cc33", 28 | statusFgColor: "#000" 29 | }, 30 | MAYBE: { 31 | iconClass: "icon-alert-triangle", 32 | iconClassPoll: "icon-status-poll-question-mark", 33 | statusClass: "s-status-warning", 34 | statusBgColor: "#ffb66c", 35 | statusFgColor: "#000" 36 | }, 37 | NO_GO: { 38 | iconClass: "icon-circle-slash", 39 | iconClassPoll: "icon-status-poll-question-mark", 40 | statusClass: "s-status-error", 41 | statusBgColor: "#9900cc", 42 | statusFgColor: "#fff" 43 | } 44 | }; 45 | const openmct = window.openmct; 46 | 47 | (() => { 48 | const POLL_INTERVAL = 100; // ms 49 | const MAX_POLL_TIME = 10000; // 10 seconds 50 | const COMPOSITION_RETRY_DELAY = 250; // ms 51 | const MAX_COMPOSITION_RETRIES = 20; // 5 seconds total with 250ms intervals 52 | const ONE_SECOND = 1000; 53 | const ONE_MINUTE = ONE_SECOND * 60; 54 | const THIRTY_MINUTES = ONE_MINUTE * 30; 55 | 56 | openmct.setAssetPath("/node_modules/openmct/dist"); 57 | 58 | installDefaultPlugins(); 59 | openmct.install(installYamcsPlugins(config)); 60 | openmct.install( 61 | openmct.plugins.OperatorStatus({ statusStyles: STATUS_STYLES }) 62 | ); 63 | 64 | document.addEventListener("DOMContentLoaded", function () { 65 | openmct.start(); 66 | }); 67 | openmct.install( 68 | openmct.plugins.Conductor({ 69 | menuOptions: [ 70 | { 71 | name: "Realtime", 72 | timeSystem: "utc", 73 | clock: "local", 74 | clockOffsets: { 75 | start: -THIRTY_MINUTES, 76 | end: 0 77 | } 78 | }, 79 | { 80 | name: "Fixed", 81 | timeSystem: "utc", 82 | bounds: { 83 | start: Date.now() - THIRTY_MINUTES, 84 | end: 0 85 | } 86 | } 87 | ] 88 | }) 89 | ); 90 | 91 | function installDefaultPlugins() { 92 | openmct.install(openmct.plugins.LocalStorage()); 93 | openmct.install(openmct.plugins.Espresso()); 94 | openmct.install(openmct.plugins.MyItems()); 95 | openmct.install(openmct.plugins.example.Generator()); 96 | openmct.install(openmct.plugins.example.ExampleImagery()); 97 | openmct.install(openmct.plugins.UTCTimeSystem()); 98 | openmct.install(openmct.plugins.TelemetryMean()); 99 | 100 | openmct.install( 101 | openmct.plugins.DisplayLayout({ 102 | showAsView: ["summary-widget", "example.imagery", "yamcs.image"] 103 | }) 104 | ); 105 | openmct.install(openmct.plugins.SummaryWidget()); 106 | openmct.install(openmct.plugins.Notebook()); 107 | openmct.install(openmct.plugins.LADTable()); 108 | openmct.install( 109 | openmct.plugins.ClearData([ 110 | "table", 111 | "telemetry.plot.overlay", 112 | "telemetry.plot.stacked" 113 | ]) 114 | ); 115 | 116 | openmct.install(openmct.plugins.FaultManagement()); 117 | openmct.install(openmct.plugins.BarChart()); 118 | openmct.install(openmct.plugins.Timeline()); 119 | openmct.install(openmct.plugins.EventTimestripPlugin()); 120 | 121 | // setup example display layout 122 | openmct.on('start', async () => { 123 | if (localStorage.getItem('exampleLayout') !== null) { 124 | return; 125 | } 126 | 127 | // try to import the example display layout, fail gracefully 128 | try { 129 | // Function to fetch JSON content as text 130 | async function fetchJsonText(url) { 131 | const response = await fetch(url); 132 | const text = await response.text(); 133 | 134 | return text; 135 | } 136 | 137 | async function getExampleLayoutPath() { 138 | const objects = Object.values(JSON.parse(localStorage.getItem('mct'))); 139 | const exampleLayout = objects.find(object => object.name === 'Example Flexible Layout'); 140 | let path = await openmct.objects.getOriginalPath(exampleLayout.identifier); 141 | 142 | path.pop(); 143 | path = path.reverse(); 144 | path = path.reduce((prev, curr) => { 145 | return prev + '/' + openmct.objects.makeKeyString(curr.identifier); 146 | }, '#/browse'); 147 | 148 | return path; 149 | } 150 | 151 | // poll for the localStorage item 152 | function mctItemExists() { 153 | return new Promise((resolve, reject) => { 154 | const startTime = Date.now(); 155 | 156 | function checkItem() { 157 | if (localStorage.getItem('mct') !== null) { 158 | resolve(true); 159 | 160 | return; 161 | } 162 | 163 | if (Date.now() - startTime > MAX_POLL_TIME) { 164 | reject(new Error('Timeout waiting for mct localStorage item')); 165 | 166 | return; 167 | } 168 | 169 | setTimeout(checkItem, POLL_INTERVAL); 170 | } 171 | 172 | checkItem(); 173 | }); 174 | } 175 | 176 | // wait for the 'mct' item to exist 177 | await mctItemExists(); 178 | 179 | // setup to use import as JSON action 180 | const importAction = openmct.actions.getAction('import.JSON'); 181 | const myItems = await openmct.objects.get('mine'); 182 | const exampleDisplayText = await fetchJsonText('./example-display.json'); 183 | const selectFile = { 184 | body: exampleDisplayText 185 | }; 186 | 187 | // import the example display layout 188 | importAction.onSave(myItems, { selectFile }); 189 | 190 | // the importAction has asynchronous code, so we will need to check 191 | // the composition of My Items to confirm the import was successful 192 | const compositionCollection = openmct.composition.get(myItems); 193 | let compositionLength = 0; 194 | let composition; 195 | 196 | let retryCount = 0; 197 | while (compositionLength === 0 && retryCount < MAX_COMPOSITION_RETRIES) { 198 | composition = await compositionCollection.load(); 199 | compositionLength = composition.length; 200 | 201 | if (compositionLength === 0) { 202 | retryCount++; 203 | await new Promise(resolve => setTimeout(resolve, COMPOSITION_RETRY_DELAY)); 204 | } 205 | } 206 | 207 | if (compositionLength === 0) { 208 | throw new Error('Failed to load composition after maximum retries'); 209 | } 210 | 211 | const exampleLayoutPath = await getExampleLayoutPath(); 212 | 213 | // give everything time to initialize 214 | await new Promise(resolve => setTimeout(resolve, 250)); 215 | 216 | openmct.notifications.info('Navigated to Example Display Layout'); 217 | 218 | // navigate to the "Example Display Layout" 219 | openmct.router.navigate(exampleLayoutPath); 220 | 221 | // set the localStorage item to prevent this from running again 222 | localStorage.setItem('exampleLayout', 'true'); 223 | } catch (error) { 224 | console.error('Failed to set up example display layout:', error); 225 | openmct.notifications.error('Failed to load example display layout: ' + error.message); 226 | } 227 | }); 228 | } 229 | })(); 230 | -------------------------------------------------------------------------------- /src/providers/mission-status/mission-status-telemetry.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * Open MCT, Copyright (c) 2014-2024, United States Government 3 | * as represented by the Administrator of the National Aeronautics and Space 4 | * Administration. All rights reserved. 5 | * 6 | * Open MCT is licensed under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0. 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | * License for the specific language governing permissions and limitations 15 | * under the License. 16 | * 17 | * Open MCT includes source code licensed under additional open source 18 | * licenses. See the Open Source Licenses file (LICENSES.md) included with 19 | * this source code distribution or the Licensing information page available 20 | * at runtime from the About dialog for additional information. 21 | *****************************************************************************/ 22 | import { 23 | idToQualifiedName 24 | } from '../../utils.js'; 25 | 26 | export default class MissionStatusTelemetry { 27 | #missionStatusMap; 28 | #missionActions; 29 | /** @type {Set} */ 30 | #missionStatusParameterNames; 31 | #missionActionToTelemetryObjectMap; 32 | #setReady; 33 | #readyPromise; 34 | #url; 35 | #instance; 36 | #processor; 37 | #openmct; 38 | 39 | constructor(openmct, { url, instance, processor = 'realtime' }) { 40 | this.#missionStatusMap = {}; 41 | this.#missionActions = new Set(); 42 | this.#missionStatusParameterNames = new Set(); 43 | this.#missionActionToTelemetryObjectMap = {}; 44 | this.#readyPromise = new Promise((resolve) => this.#setReady = resolve); 45 | this.#url = url; 46 | this.#instance = instance; 47 | this.#processor = processor; 48 | this.#openmct = openmct; 49 | } 50 | 51 | /** 52 | * Set the status for a particular mission action. 53 | * @param {MissionAction} action the mission action 54 | * @param {MissionStatus} status the status 55 | * @returns {Promise} true if the status was set successfully 56 | */ 57 | async setStatusForMissionAction(action, status) { 58 | const telemetryObject = await this.getTelemetryObjectForAction(action); 59 | const setParameterUrl = this.#buildUrl(telemetryObject.identifier); 60 | let success = false; 61 | 62 | try { 63 | const result = await fetch(setParameterUrl, { 64 | method: 'PUT', 65 | headers: { 66 | 'Content-Type': 'application/json' 67 | }, 68 | body: JSON.stringify({ 69 | type: 'SINT64', 70 | sint64Value: status.key 71 | }) 72 | }); 73 | 74 | success = result.ok === true; 75 | } catch (error) { 76 | console.error(error); 77 | } 78 | 79 | return success; 80 | } 81 | 82 | /** 83 | * Get the possible mission statuses. 84 | * i.e: "Go" or "No Go" 85 | * @returns {Promise} 86 | */ 87 | async getPossibleMissionStatuses() { 88 | await this.#readyPromise; 89 | 90 | return Object.values(this.#missionStatusMap).map(status => this.toMissionStatusFromMdbEntry(status)); 91 | } 92 | 93 | /** 94 | * Get the default status for any mission action. 95 | * Returns the first status in the list of possible statuses. 96 | * @returns {Promise} 97 | */ 98 | async getDefaultStatusForAction() { 99 | const possibleStatuses = await this.getPossibleMissionStatuses(); 100 | 101 | return possibleStatuses[0]; 102 | } 103 | 104 | /** 105 | * Adds a mission status to the list of possible statuses. 106 | * @param {MissionStatus} status 107 | */ 108 | addStatus(status) { 109 | this.#missionStatusMap[status.value] = status; 110 | } 111 | 112 | /** 113 | * Get the telemetry object for a mission action. 114 | * @param {MissionAction} action the mission action 115 | * @returns {Promise} the telemetry object 116 | */ 117 | async getTelemetryObjectForAction(action) { 118 | await this.#readyPromise; 119 | 120 | return this.#missionActionToTelemetryObjectMap[action]; 121 | } 122 | 123 | /** 124 | * Check if this parameter name is a mission status parameter name. 125 | * @param {string} parameterName 126 | * @returns {boolean} true if the parameter name is a mission status parameter name 127 | */ 128 | async isMissionStatusParameterName(parameterName) { 129 | await this.#readyPromise; 130 | if (this.#missionStatusParameterNames.has(parameterName)) { 131 | return true; 132 | } 133 | 134 | const parameterRegExp = new RegExp(`^${parameterName}$`); 135 | for (const missionStatusParameterName of this.#missionStatusParameterNames) { 136 | if (parameterRegExp.test(missionStatusParameterName)) { 137 | return true; 138 | } 139 | } 140 | 141 | return false; 142 | } 143 | 144 | /** 145 | * Set the telemetry object for a mission action. 146 | * @param {MissionAction} action 147 | * @param {TelemetryObject} telemetryObject 148 | */ 149 | setTelemetryObjectForAction(action, telemetryObject) { 150 | this.#missionActionToTelemetryObjectMap[action] = telemetryObject; 151 | } 152 | 153 | /** 154 | * Add a mission action to the list of possible actions. 155 | * @param {MissionAction} action 156 | */ 157 | addMissionAction(action) { 158 | this.#missionActions.add(action); 159 | } 160 | 161 | /** 162 | * Add a mission status parameter name to the list of parameter names. 163 | * @param {string} parameterName 164 | */ 165 | addMissionStatusParameterName(parameterName) { 166 | this.#missionStatusParameterNames.add(parameterName); 167 | } 168 | 169 | /** 170 | * Get a list of all mission actions. 171 | * @returns {Promise} 172 | */ 173 | async getAllMissionActions() { 174 | await this.#readyPromise; 175 | 176 | return Array.from(this.#missionActions); 177 | } 178 | 179 | /** 180 | * Get the current status of a mission action given its MDB entry. 181 | * @param {MdbEntry} yamcsStatus the MDB entry 182 | * @returns {MissionStatus} 183 | */ 184 | toMissionStatusFromMdbEntry(yamcsStatus) { 185 | return { 186 | // eslint-disable-next-line radix 187 | key: parseInt(yamcsStatus.value), 188 | label: yamcsStatus.label 189 | }; 190 | } 191 | 192 | /** 193 | * Receives a telemetry object and a datum and returns a mission status. 194 | * @param {TelemetryObject} telemetryObject the telemetry object 195 | * @param {Datum} datum the datum object 196 | * @returns {MissionStatus} 197 | */ 198 | toStatusFromTelemetry(telemetryObject, datum) { 199 | const metadata = this.#openmct.telemetry.getMetadata(telemetryObject); 200 | const rangeMetadata = metadata.valuesForHints(['range'])[0]; 201 | const formatter = this.#openmct.telemetry.getValueFormatter(rangeMetadata); 202 | const timestampMetadata = metadata.valuesForHints(['domain'])[0]; 203 | const dateFormatter = this.#openmct.telemetry.getValueFormatter(timestampMetadata); 204 | 205 | return { 206 | key: formatter.parse(datum), 207 | label: formatter.format(datum), 208 | timestamp: dateFormatter.parse(datum) 209 | }; 210 | } 211 | 212 | /** 213 | * Fires when the dictionary is loaded. 214 | */ 215 | dictionaryLoadComplete() { 216 | this.#setReady(); 217 | } 218 | 219 | /** 220 | * Construct the URL for a parameter. 221 | * @param {import('openmct').Identifier} id the identifier 222 | * @returns {string} 223 | */ 224 | #buildUrl(id) { 225 | let url = `${this.#url}api/processors/${this.#instance}/${this.#processor}/parameters/${idToQualifiedName(id.key)}`; 226 | 227 | return url; 228 | } 229 | } 230 | 231 | /** 232 | * @typedef {Object} MissionStatus 233 | * @property {number} key 234 | * @property {string} label 235 | * @property {number?} timestamp 236 | */ 237 | 238 | /** 239 | * @typedef {string} MissionAction 240 | */ 241 | 242 | /** 243 | * @typedef {Object} TelemetryObject 244 | * @property {import('openmct').Identifier} identifier 245 | * @property {string} name 246 | * @property {string} type 247 | * @property {string} location 248 | * @property {string} configuration 249 | * @property {string} domain 250 | * @property {object} telemetry 251 | * @property {TelemetryValue[]} telemetry.values 252 | * @property {string} metadata 253 | * @property {string} composition 254 | * @property {string} object 255 | * @property {string} value 256 | */ 257 | 258 | /** 259 | * @typedef {object} TelemetryValue 260 | * @property {string} key 261 | * @property {string} name 262 | * @property {string} format 263 | * @property {string} source 264 | * @property {object} hints 265 | * @property {number} hints.domain 266 | */ 267 | 268 | /** 269 | * @typedef {object} Datum 270 | * @property {string} id 271 | * @property {string} timestamp 272 | * @property {string} acquisitionStatus 273 | * @property {*} value 274 | */ 275 | 276 | /** 277 | * @typedef {object} MdbEntry 278 | * @property {string} value 279 | * @property {string} label 280 | * @property {string} description 281 | */ 282 | --------------------------------------------------------------------------------