Skip to content

Commit 4ab98ff

Browse files
jvigliottaakhenry
andauthored
Add Correlation Telemetry Plugin (#8216)
* adding the plugin file * adding to available plugins list * this update makes it so you can see the items selected for correlation in the edit form, previously they would be blank * fixed incorrect formatting for locator field vue compnent, removed unnecessary openmct arg from correlation plugin * spelling fix * add test for correlated telemetry, update subscribe to telemetry util function and move to app actions * checking timestamps match so we are correctly checking correlated telemetry * Rename utility function to align with its implementation details --------- Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: Andrew Henry <andrew.k.henry@nasa.gov>
1 parent a0a06cf commit 4ab98ff

7 files changed

Lines changed: 389 additions & 61 deletions

File tree

e2e/appActions.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,45 @@ async function renameCurrentObjectFromBrowseBar(page, newName) {
708708
await page.getByLabel('Browse bar', { exact: true }).click();
709709
}
710710

711+
/**
712+
* Util for subscribing to a telemetry object by object identifier
713+
* Limitations: Currently only works to return telemetry once to the node scope
714+
* To Do: See if there's a way to await this multiple times to allow for multiple
715+
* values to be returned over time
716+
* @param {import('@playwright/test').Page} page
717+
* @param {string} objectIdentifier identifier for object
718+
* @returns {Promise<string>} the formatted sin telemetry value
719+
*/
720+
async function getNextSineValueFromSWG(page, objectIdentifier, returnOnlyValue = true) {
721+
// Generate a unique function name for this subscription
722+
const uniqueFunctionName = `getTelemValue_${genUuid().replace(/-/g, '_')}`;
723+
724+
const getTelemValuePromise = new Promise((resolve) =>
725+
page.exposeFunction(uniqueFunctionName, resolve)
726+
);
727+
728+
await page.evaluate(
729+
async ({ telemetryIdentifier, functionName, onlyValue }) => {
730+
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
731+
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
732+
const formats = await window.openmct.telemetry.getFormatMap(metadata);
733+
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
734+
const sinVal = obj.sin;
735+
const formattedSinVal = formats.sin.format(sinVal);
736+
const formattedTimestamp = formats.utc.format(obj.utc);
737+
window[functionName](onlyValue ? formattedSinVal : { ...obj, formattedTimestamp });
738+
});
739+
},
740+
{
741+
telemetryIdentifier: objectIdentifier,
742+
functionName: uniqueFunctionName,
743+
onlyValue: returnOnlyValue
744+
}
745+
);
746+
747+
return getTelemValuePromise;
748+
}
749+
711750
export {
712751
createDomainObjectWithDefaults,
713752
createExampleTelemetryObject,
@@ -716,6 +755,7 @@ export {
716755
createStableStateTelemetry,
717756
expandEntireTree,
718757
getCanvasPixels,
758+
getNextSineValueFromSWG,
719759
linkParameterToObject,
720760
navigateToObjectWithFixedTimeBounds,
721761
navigateToObjectWithRealTime,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
createDomainObjectWithDefaults,
25+
getNextSineValueFromSWG,
26+
setRealTimeMode
27+
} from '../../../../appActions.js';
28+
import { expect, test } from '../../../../pluginFixtures.js';
29+
30+
test.describe('Correlation Telemetry', () => {
31+
test.beforeEach(async ({ page }) => {
32+
await page.goto('./', { waitUntil: 'domcontentloaded' });
33+
await page.evaluate(() => {
34+
const openmct = window.openmct;
35+
openmct.install(openmct.plugins.CorrelationTelemetry());
36+
});
37+
});
38+
39+
test('will correlate telemetry from two objects based on timestamp', async ({ page }) => {
40+
const sineWaveGenerator1 = await createDomainObjectWithDefaults(page, {
41+
type: 'Sine Wave Generator',
42+
name: 'Sine Wave Generator 1'
43+
});
44+
const sineWaveGenerator2 = await createDomainObjectWithDefaults(page, {
45+
type: 'Sine Wave Generator',
46+
name: 'Sine Wave Generator 2'
47+
});
48+
49+
// create correlation telemetry object, with x and y sources
50+
await page.getByRole('button', { name: 'Create', exact: true }).click();
51+
await page.getByRole('menuitem', { name: 'Correlation Telemetry' }).click();
52+
await page.getByLabel('Title', { exact: true }).fill('');
53+
await page.getByLabel('Title', { exact: true }).fill('Test Correlation Telemetry');
54+
55+
// choose sine wave generator 1 as x source
56+
const createModalTreeFirst = page.getByLabel('Create Modal Tree').first();
57+
await createModalTreeFirst.getByLabel('Expand My Items folder').click();
58+
await createModalTreeFirst
59+
.getByLabel('Navigate to Sine Wave Generator 1 generator Object')
60+
.click();
61+
62+
// choose sine wave generator 2 as y source
63+
const createModalTreeSecond = page.getByLabel('Create Modal Tree').nth(1);
64+
await createModalTreeSecond.getByLabel('Expand My Items folder').click();
65+
await createModalTreeSecond
66+
.getByLabel('Navigate to Sine Wave Generator 2 generator Object')
67+
.click();
68+
69+
// save in my items folder
70+
const createModalTreeThird = page.getByLabel('Create Modal Tree').nth(2);
71+
await createModalTreeThird.getByLabel('Navigate to My Items folder').click();
72+
await page.getByRole('button', { name: 'Save' }).click();
73+
74+
await setRealTimeMode(page);
75+
76+
await page.getByLabel('Open the View Switcher Menu').click();
77+
await page.getByLabel('Telemetry Table').click();
78+
79+
const getSWG1ValuePromise = getNextSineValueFromSWG(page, sineWaveGenerator1.uuid, false);
80+
const getSWG2ValuePromise = getNextSineValueFromSWG(page, sineWaveGenerator2.uuid, false);
81+
82+
const swg1Value = await getSWG1ValuePromise;
83+
const swg2Value = await getSWG2ValuePromise;
84+
const correlatedTelemetryObject = {
85+
x: swg1Value.sin,
86+
y: swg2Value.sin,
87+
formattedTimestamp: swg1Value.formattedTimestamp,
88+
timestampsMatch: swg1Value.utc === swg2Value.utc
89+
};
90+
91+
// wait for correlated telemetry object formatted timestamp to be visible in the telemetry table
92+
await expect(page.getByText(correlatedTelemetryObject.formattedTimestamp)).toBeVisible();
93+
await expect(correlatedTelemetryObject.timestampsMatch).toBe(true);
94+
95+
// check that the x and y values are correlated in the same row, based on column names: x and y, respectively
96+
const telemetryTableRows = page.getByRole('row');
97+
const correlatedRow = telemetryTableRows.filter((row) =>
98+
row.getByText(correlatedTelemetryObject.formattedTimestamp).isVisible()
99+
);
100+
await expect(
101+
correlatedRow.getByLabel(`x table cell ${correlatedTelemetryObject.x}`).first()
102+
).toBeVisible();
103+
await expect(
104+
correlatedRow.getByLabel(`y table cell ${correlatedTelemetryObject.y}`).first()
105+
).toBeVisible();
106+
});
107+
});

e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
2323

2424
import {
2525
createDomainObjectWithDefaults,
26+
getNextSineValueFromSWG,
2627
navigateToObjectWithFixedTimeBounds,
2728
setFixedIndependentTimeConductorBounds,
2829
setFixedTimeMode,
@@ -242,7 +243,7 @@ test.describe('Display Layout', () => {
242243
// Subscribe to the Sine Wave Generator data
243244
// On getting data, check if the value found in the Display Layout is the most recent value
244245
// from the Sine Wave Generator
245-
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
246+
const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);
246247
const formattedTelemetryValue = await getTelemValuePromise;
247248
await expect(page.getByText(formattedTelemetryValue)).toBeVisible();
248249
const displayLayoutValue = await page.getByText(formattedTelemetryValue).textContent();
@@ -282,7 +283,7 @@ test.describe('Display Layout', () => {
282283
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
283284

284285
// Subscribe to the Sine Wave Generator data
285-
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
286+
const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);
286287
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
287288
await setStartOffset(page, { startMins: '1' });
288289
await setFixedTimeMode(page);
@@ -693,31 +694,3 @@ async function addLayoutObject(page, layoutName, layoutObject) {
693694
await page.getByText('Ok').click();
694695
}
695696
}
696-
697-
/**
698-
* Util for subscribing to a telemetry object by object identifier
699-
* Limitations: Currently only works to return telemetry once to the node scope
700-
* To Do: See if there's a way to await this multiple times to allow for multiple
701-
* values to be returned over time
702-
* @param {import('@playwright/test').Page} page
703-
* @param {string} objectIdentifier identifier for object
704-
* @returns {Promise<string>} the formatted sin telemetry value
705-
*/
706-
async function subscribeToTelemetry(page, objectIdentifier) {
707-
const getTelemValuePromise = new Promise((resolve) =>
708-
page.exposeFunction('getTelemValue', resolve)
709-
);
710-
711-
await page.evaluate(async (telemetryIdentifier) => {
712-
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
713-
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
714-
const formats = await window.openmct.telemetry.getFormatMap(metadata);
715-
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
716-
const sinVal = obj.sin;
717-
const formattedSinVal = formats.sin.format(sinVal);
718-
window.getTelemValue(formattedSinVal);
719-
});
720-
}, objectIdentifier);
721-
722-
return getTelemValuePromise;
723-
}

e2e/tests/functional/plugins/lad/lad.e2e.spec.js

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import {
2424
createDomainObjectWithDefaults,
25+
getNextSineValueFromSWG,
2526
navigateToObjectWithRealTime,
2627
setFixedTimeMode,
2728
setRealTimeMode,
@@ -268,7 +269,7 @@ test.describe('Testing LAD table', () => {
268269
// Subscribe to the Sine Wave Generator data
269270
// On getting data, check if the value found in the LAD table is the most recent value
270271
// from the Sine Wave Generator
271-
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
272+
const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);
272273
const subscribeTelemValue = await getTelemValuePromise;
273274
await expect(page.getByLabel('lad value')).toHaveText(subscribeTelemValue);
274275
const ladTableValue = await page.getByText(subscribeTelemValue).textContent();
@@ -294,7 +295,7 @@ test.describe('Testing LAD table', () => {
294295
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
295296

296297
// Subscribe to the Sine Wave Generator data
297-
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
298+
const getTelemValuePromise = getNextSineValueFromSWG(page, sineWaveObject.uuid);
298299
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
299300
await setRealTimeMode(page);
300301
await setStartOffset(page, { startMins: '01' });
@@ -307,34 +308,6 @@ test.describe('Testing LAD table', () => {
307308
});
308309
});
309310

310-
/**
311-
* Util for subscribing to a telemetry object by object identifier
312-
* Limitations: Currently only works to return telemetry once to the node scope
313-
* To Do: See if there's a way to await this multiple times to allow for multiple
314-
* values to be returned over time
315-
* @param {import('@playwright/test').Page} page
316-
* @param {string} objectIdentifier identifier for object
317-
* @returns {Promise<string>} the formatted sin telemetry value
318-
*/
319-
async function subscribeToTelemetry(page, objectIdentifier) {
320-
const getTelemValuePromise = new Promise((resolve) =>
321-
page.exposeFunction('getTelemValue', resolve)
322-
);
323-
324-
await page.evaluate(async (telemetryIdentifier) => {
325-
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
326-
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
327-
const formats = await window.openmct.telemetry.getFormatMap(metadata);
328-
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
329-
const sinVal = obj.sin;
330-
const formattedSinVal = formats.sin.format(sinVal);
331-
window.getTelemValue(formattedSinVal);
332-
});
333-
}, objectIdentifier);
334-
335-
return getTelemValuePromise;
336-
}
337-
338311
/**
339312
* Open the given `domainObject`'s context menu from the object tree.
340313
* Expands the path to the object and scrolls to it if necessary.

src/api/forms/components/controls/LocatorField.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<template>
2424
<MctTree
2525
:is-selector-tree="true"
26-
:initial-selection="model.parent"
26+
:initial-selection="initialSelection"
2727
@tree-item-selection="handleItemSelection"
2828
/>
2929
</template>
@@ -43,6 +43,11 @@ export default {
4343
}
4444
},
4545
emits: ['on-change'],
46+
computed: {
47+
initialSelection() {
48+
return this.model.parent || this.model.value?.[0];
49+
}
50+
},
4651
methods: {
4752
handleItemSelection(item) {
4853
const data = {

0 commit comments

Comments
 (0)