Add realtime output of telemetry data in conditionals and add support for historical conditional telemetry queries to allow for plotting

This commit is contained in:
Khalid Adil 2024-09-19 14:36:57 -05:00
parent 73489cd78d
commit 23cf829fdc
9 changed files with 510 additions and 21 deletions

View File

@ -24,6 +24,7 @@ import { EventEmitter } from 'eventemitter3';
import { v4 as uuid } from 'uuid';
import Condition from './Condition.js';
import HistoricalTelemetryProvider from './historicalTelemetryProvider.js';
import { getLatestTimestamp } from './utils/time.js';
export default class ConditionManager extends EventEmitter {
@ -46,6 +47,8 @@ export default class ConditionManager extends EventEmitter {
applied: false
};
this.initialize();
this.telemetryBuffer = [];
this.isProcessing = false;
}
subscribeToTelemetry(telemetryObject) {
@ -320,6 +323,17 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
getHistoricalData() {
const historicalTelemetry = new HistoricalTelemetryProvider(
this.openmct,
this.telemetryObjects,
this.compositionLoad,
this.conditions,
this.conditionSetDomainObject
);
return historicalTelemetry.getHistoricalData();
}
getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length - 1];
@ -375,8 +389,26 @@ export default class ConditionManager extends EventEmitter {
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
let output = currentCondition?.configuration?.output;
if (output === 'telemetry value') {
const { outputTelemetry, outputMetadata } = currentCondition.configuration;
const outputTelemetryObject = await this.openmct.objects.get(outputTelemetry);
const telemetryOptions = {
size: 1,
strategy: 'latest',
timeContext: this.openmct.time.getContextForView([])
};
const latestData = await this.openmct.telemetry.request(
outputTelemetryObject,
telemetryOptions
);
if (latestData?.[0]?.[outputMetadata]) {
output = latestData?.[0]?.[outputMetadata];
}
}
const currentOutput = {
output: currentCondition.configuration.output,
output: output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id,
...latestTimestamp
@ -394,6 +426,18 @@ export default class ConditionManager extends EventEmitter {
}
}
const conditionTelemetries = [];
const conditions = this.conditionSetDomainObject.configuration.conditionCollection;
conditions.forEach((condition) => {
if (condition?.configuration?.outputTelemetry) {
conditionTelemetries.push(condition?.configuration?.outputTelemetry);
}
});
if (conditionTelemetries.includes(id)) {
return true;
}
return false;
}
@ -415,7 +459,7 @@ export default class ConditionManager extends EventEmitter {
timestamp[timeSystemKey] = currentTimestamp;
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
this.updateConditionResults(normalizedDatum);
this.updateCurrentCondition(timestamp);
this.updateCurrentCondition(timestamp, endpoint, datum);
}
}
@ -428,14 +472,12 @@ export default class ConditionManager extends EventEmitter {
});
}
updateCurrentCondition(timestamp) {
const currentCondition = this.getCurrentCondition();
emitConditionSetResult(currentCondition, timestamp, outputValue) {
this.emit(
'conditionSetResultUpdated',
Object.assign(
{
output: currentCondition.configuration.output,
output: outputValue,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
@ -444,6 +486,60 @@ export default class ConditionManager extends EventEmitter {
);
}
updateCurrentCondition(timestamp, telemetryObject, telemetryData) {
this.telemetryBuffer.push({ timestamp, telemetryObject, telemetryData });
if (!this.isProcessing) {
this.processBuffer();
}
}
async processBuffer() {
this.isProcessing = true;
while (this.telemetryBuffer.length > 0) {
const { timestamp, telemetryObject, telemetryData } = this.telemetryBuffer.shift();
await this.processCondition(timestamp, telemetryObject, telemetryData);
}
this.isProcessing = false;
}
async processCondition(timestamp, telemetryObject, telemetryData) {
const currentCondition = this.getCurrentCondition();
let telemetryValue = currentCondition.configuration.output;
if (currentCondition?.configuration?.outputTelemetry) {
const selectedOutputIdentifier = currentCondition?.configuration?.outputTelemetry;
const outputMetadata = currentCondition?.configuration?.outputMetadata;
const telemetryKeystring = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (selectedOutputIdentifier === telemetryKeystring) {
telemetryValue = telemetryData[outputMetadata];
} else {
const outputTelemetryObject = await this.openmct.objects.get(selectedOutputIdentifier);
const telemetryOptions = {
size: 1,
strategy: 'latest',
start: timestamp?.utc - 1000,
end: timestamp?.utc + 1000
};
const outputTelemetryData = await this.openmct.telemetry.request(
outputTelemetryObject,
telemetryOptions
);
const outputTelemetryValue =
outputTelemetryData?.length > 0 ? outputTelemetryData.slice(-1)[0] : null;
if (outputTelemetryData.length && outputTelemetryValue?.[outputMetadata]) {
telemetryValue = outputTelemetryValue?.[outputMetadata];
} else {
telemetryValue = undefined;
}
}
}
this.emitConditionSetResult(currentCondition, timestamp, telemetryValue);
}
getTestData(metadatum) {
let data = undefined;
if (this.testData.applied) {

View File

@ -42,8 +42,9 @@ export default class ConditionSetTelemetryProvider {
async request(domainObject, options) {
let conditionManager = this.getConditionManager(domainObject);
const formattedHistoricalData = await conditionManager.getHistoricalData();
let latestOutput = await conditionManager.requestLADConditionSetOutput(options);
return latestOutput;
return [...formattedHistoricalData, ...latestOutput];
}
subscribe(domainObject, callback) {

View File

@ -235,10 +235,11 @@ export default {
return arr;
},
addTelemetryObject(domainObject) {
async addTelemetryObject(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const telemetryPath = await this.getFullTelemetryPath(domainObject);
this.telemetryObjs.push(domainObject);
this.telemetryObjs.push({ ...domainObject, path: telemetryPath });
this.$emit('telemetry-updated', this.telemetryObjs);
this.subscribeToStaleness(domainObject, (stalenessResponse) => {
@ -248,6 +249,19 @@ export default {
});
});
},
async getFullTelemetryPath(telemetry) {
const keyString = this.openmct.objects.makeKeyString(telemetry.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(keyString, []);
const telemetryPath = originalPathObjects.reverse().map((pathObject) => {
if (pathObject.type !== 'root') {
return pathObject.name;
}
return undefined;
});
return telemetryPath.join('/');
},
removeTelemetryObject(identifier) {
const keyString = this.openmct.objects.makeKeyString(identifier);
const index = this.telemetryObjs.findIndex((obj) => {

View File

@ -99,13 +99,13 @@
@change="setOutputValue"
>
<option v-for="option in outputOptions" :key="option" :value="option">
{{ initCap(option) }}
{{ option }}
</option>
</select>
</span>
<span class="c-cdef__control">
<input
v-if="selectedOutputSelection === outputOptions[2]"
v-if="selectedOutputSelection === outputOptions[3]"
v-model="condition.configuration.output"
aria-label="Condition Output String"
class="t-condition-name-input"
@ -113,8 +113,41 @@
@change="persist"
/>
</span>
<span v-if="selectedOutputSelection === 'telemetry value'" class="c-cdef__control">
<select
v-model="condition.configuration.outputTelemetry"
aria-label="Output Telemetry Selection"
@change="persist"
>
<option value="">- Select Telemetry -</option>
<option
v-for="telemetryOption in telemetry"
:key="openmct.objects.makeKeyString(telemetryOption.identifier)"
:value="openmct.objects.makeKeyString(telemetryOption.identifier)"
>
{{ telemetryOption.path }}
</option>
</select>
</span>
<span v-if="condition.configuration.outputTelemetry" class="c-cdef__control">
<select
v-model="condition.configuration.outputMetadata"
aria-label="Output Telemetry Metadata Selection"
@change="persist"
>
<option value="">- Select Field -</option>
<option
v-for="(option, index) in telemetryMetadataOptions[
condition.configuration.outputTelemetry
]"
:key="index"
:value="option.key"
>
{{ option.name }}
</option>
</select>
</span>
</span>
<div v-if="!condition.isDefault" class="c-cdef__match-and-criteria">
<span class="c-cdef__separator c-row-separator"></span>
<span class="c-cdef__label">Match</span>
@ -181,7 +214,12 @@
<span class="c-condition__name">
{{ condition.configuration.name }}
</span>
<span class="c-condition__output"> Output: {{ condition.configuration.output }} </span>
<span class="c-condition__output">
Output:
{{
condition.configuration.output === undefined ? 'none' : condition.configuration.output
}}
</span>
</div>
<div class="c-condition__summary">
<ConditionDescription :show-label="false" :condition="condition" />
@ -250,10 +288,11 @@ export default {
expanded: true,
trigger: 'all',
selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'],
outputOptions: ['none', 'false', 'true', 'string', 'telemetry value'],
criterionIndex: 0,
draggingOver: false,
isDefault: this.condition.isDefault
isDefault: this.condition.isDefault,
telemetryMetadataOptions: {}
};
},
computed: {
@ -287,26 +326,51 @@ export default {
return false;
}
},
watch: {
condition: {
handler() {
if (this.condition.configuration.output !== 'telemetry value') {
this.condition.configuration.outputTelemetry = null;
this.condition.configuration.outputMetadata = null;
}
},
deep: true
},
isEditing(newValue, oldValue) {
if (newValue === true) {
this.initializeMetadata();
}
}
},
unmounted() {
this.destroy();
},
mounted() {
this.setOutputSelection();
this.initializeMetadata();
},
methods: {
setOutputSelection() {
let conditionOutput = this.condition.configuration.output;
if (conditionOutput) {
if (conditionOutput !== 'false' && conditionOutput !== 'true') {
if (
conditionOutput !== 'false' &&
conditionOutput !== 'true' &&
conditionOutput !== 'telemetry value'
) {
this.selectedOutputSelection = 'string';
} else {
this.selectedOutputSelection = conditionOutput;
}
} else if (conditionOutput === undefined) {
this.selectedOutputSelection = 'none';
}
},
setOutputValue() {
if (this.selectedOutputSelection === 'string') {
this.condition.configuration.output = '';
} else if (this.selectedOutputSelection === 'none') {
this.condition.configuration.output = undefined;
} else {
this.condition.configuration.output = this.selectedOutputSelection;
}
@ -401,6 +465,24 @@ export default {
},
initCap(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
},
initializeMetadata() {
this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (telemetryMetadata) {
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
} else {
this.telemetryMetadataOptions[id] = [];
}
});
},
getId(identifier) {
if (identifier) {
return this.openmct.objects.makeKeyString(identifier);
}
return [];
}
}
};

View File

@ -40,7 +40,7 @@
:key="telemetryOption.identifier.key"
:value="telemetryOption.identifier"
>
{{ telemetryOption.name }}
{{ telemetryOption.path }}
</option>
</select>
</span>

View File

@ -63,7 +63,7 @@
:key="index"
:value="telemetryOption.identifier"
>
{{ telemetryOption.name }}
{{ telemetryPaths[index] || telemetryOption.name }}
</option>
</select>
</span>
@ -147,7 +147,8 @@ export default {
expanded: true,
isApplied: false,
testInputs: [],
telemetryMetadataOptions: {}
telemetryMetadataOptions: {},
telemetryPaths: []
};
},
watch: {
@ -200,6 +201,10 @@ export default {
this.telemetryMetadataOptions[id] = [];
}
});
this.telemetry.forEach(async (telemetryOption, index) => {
const telemetryPath = await this.getFullTelemetryPath(telemetryOption);
this.telemetryPaths[index] = telemetryPath;
});
},
addTestInput(testInput) {
this.testInputs.push(
@ -244,6 +249,22 @@ export default {
applied: this.isApplied,
conditionTestInputs: this.testInputs
});
},
async getFullTelemetryPath(telemetry) {
const keyStringForObject = this.openmct.objects.makeKeyString(telemetry.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(
keyStringForObject,
[]
);
const telemetryPath = originalPathObjects.reverse().map((pathObject) => {
if (pathObject.type !== 'root') {
return pathObject.name;
}
return undefined;
});
return telemetryPath.join('/');
}
}
};

View File

@ -0,0 +1,274 @@
export default class HistoricalTelemetryProvider {
constructor(openmct, telemetryObjects, compositionLoad, conditions, conditionSetDomainObject) {
this.openmct = openmct;
this.telemetryObjects = telemetryObjects;
this.compositionLoad = compositionLoad;
this.bounds = { start: null, end: null };
this.telemetryList = [];
this.conditions = conditions;
this.conditionSetDomainObject = conditionSetDomainObject;
this.historicalTelemetryPoolMap = new Map();
this.historicalTelemetryDateMap = new Map();
this.index = 0;
}
setTimeBounds(bounds) {
this.bounds = bounds;
}
refreshAllHistoricalTelemetries() {
const refreshPromises = [];
for (const [, value] of Object.entries(this.telemetryObjects)) {
refreshPromises.push(this.refreshHistoricalTelemetry(value));
}
return Promise.all(refreshPromises);
}
async refreshHistoricalTelemetry(domainObject, identifier) {
console.log('refreshHistoricalTelemetry');
if (!domainObject && identifier) {
domainObject = await this.openmct.objects.get(identifier);
}
const id = this.openmct.objects.makeKeyString(domainObject.identifier);
const telemetryOptions = { ...this.bounds };
const historicalTelemetry = await this.openmct.telemetry.request(
domainObject,
telemetryOptions
);
this.historicalTelemetryPoolMap.set(id, { domainObject, historicalTelemetry });
return { domainObject, historicalTelemetry };
}
evaluateTrueCondition(historicalDateMap, timestamp, condition, conditionCollectionMap) {
const telemetryData = historicalDateMap.get(timestamp);
const conditionConfiguration = conditionCollectionMap.get(condition.id)?.configuration;
const { outputTelemetry, outputMetadata } = conditionConfiguration;
let output = {};
if (outputTelemetry) {
const outputTelemetryID = this.openmct.objects.makeKeyString(outputTelemetry);
const outputTelemetryData = telemetryData.get(outputTelemetryID);
output.telemetry = outputTelemetryData;
output.value = outputTelemetryData[outputMetadata];
output.condition = condition;
} else if (conditionConfiguration?.output) {
output.telemetry = null;
output.value = conditionConfiguration?.output;
output.condition = condition;
}
return output;
}
async getAllTelemetries(conditionCollection) {
const conditionCollectionMap = new Map();
const inputTelemetries = [];
const outputTelemetries = [];
const historicalTelemetryPoolPromises = [];
conditionCollection.forEach((condition, index) => {
console.log('-------------------------');
console.log(condition);
const { id } = condition;
const { criteria, output, outputTelemetry, outputMetadata } = condition.configuration;
const inputTelemetry = criteria?.[0]?.telemetry;
console.log(id);
console.log(criteria);
console.log(output);
console.log(outputMetadata);
console.log('inputTelemetry', inputTelemetry);
console.log('outputTelemetry', outputTelemetry);
conditionCollectionMap.set(condition?.id, condition);
if (inputTelemetry) {
const inputTelemetryId = this.openmct.objects.makeKeyString(inputTelemetry);
if (![...inputTelemetries, ...outputTelemetries].includes(inputTelemetryId)) {
historicalTelemetryPoolPromises.push(
this.refreshHistoricalTelemetry(null, inputTelemetry)
);
}
inputTelemetries.push(inputTelemetryId);
} else {
inputTelemetries.push(null);
}
if (outputTelemetry) {
if (![...inputTelemetries, ...outputTelemetries].includes(outputTelemetry)) {
historicalTelemetryPoolPromises.push(
this.refreshHistoricalTelemetry(null, outputTelemetry)
);
}
outputTelemetries.push(outputTelemetry);
} else {
outputTelemetries.push(null);
}
});
const historicalTelemetriesPool = await Promise.all(historicalTelemetryPoolPromises);
return {
historicalTelemetriesPool,
inputTelemetries,
outputTelemetries,
conditionCollectionMap
};
}
sortTelemetriesByDate(historicalTelemetriesPool) {
const historicalTelemetryDateMap = new Map();
historicalTelemetriesPool.forEach((historicalTelemetryList) => {
const { historicalTelemetry, domainObject } = historicalTelemetryList;
const { identifier } = domainObject;
const telemetryIdentifier = this.openmct.objects.makeKeyString(identifier);
historicalTelemetry.forEach((historicalTelemetryItem) => {
if (!historicalTelemetryDateMap.get(historicalTelemetryItem.utc)) {
const telemetryMap = new Map();
telemetryMap.set(telemetryIdentifier, historicalTelemetryItem);
historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap);
} else {
const telemetryMap = historicalTelemetryDateMap.get(historicalTelemetryItem.utc);
telemetryMap.set(telemetryIdentifier, historicalTelemetryItem);
historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap);
}
});
});
return historicalTelemetryDateMap;
}
evaluateConditionsByDate(historicalTelemetryDateMap, conditionCollectionMap) {
const outputTelemetryDateMap = new Map();
historicalTelemetryDateMap.forEach((historicalTelemetryMap, timestamp) => {
let isConditionValid = false;
const evaluatedConditions = [];
this.conditions.forEach((condition) => {
if (isConditionValid) {
return;
}
const { id } = condition;
const conditionMetadata = { condition };
const conditionCriteria = condition.criteria[0];
let result;
if (conditionCriteria?.telemetry) {
const conditionInputTelemetryId = this.openmct.objects.makeKeyString(
conditionCriteria.telemetry
);
const inputTelemetry = historicalTelemetryMap.get(conditionInputTelemetryId);
conditionMetadata.inputTelemetry = inputTelemetry;
result = conditionCriteria.computeResult({
id,
...inputTelemetry
});
} else if (!conditionCriteria) {
const conditionDetails = conditionCollectionMap.get(id);
const { isDefault } = conditionDetails;
const conditionConfiguration = conditionDetails?.configuration;
const { outputTelemetry, outputMetadata, output } = conditionConfiguration;
if (isDefault) {
const conditionOutput = {
telemetry: null,
value: output,
condition
};
outputTelemetryDateMap.set(timestamp, conditionOutput);
}
}
conditionMetadata.result = result;
evaluatedConditions.push(conditionMetadata);
if (result === true) {
isConditionValid = true;
const conditionOutput = this.evaluateTrueCondition(
historicalTelemetryDateMap,
timestamp,
condition,
conditionCollectionMap
);
console.log(conditionOutput.value);
outputTelemetryDateMap.set(timestamp, conditionOutput);
}
});
});
return outputTelemetryDateMap;
}
async getHistoricalInputsByDate() {
console.log('getHistoricalInputsByDate');
console.log(this.conditions);
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
const {
historicalTelemetriesPool,
inputTelemetries,
outputTelemetries,
conditionCollectionMap
} = await this.getAllTelemetries(conditionCollection);
const historicalTelemetryDateMap = this.sortTelemetriesByDate(historicalTelemetriesPool);
const outputTelemetryDateMap = this.evaluateConditionsByDate(
historicalTelemetryDateMap,
conditionCollectionMap
);
console.log('*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣');
console.log(historicalTelemetriesPool);
console.log(this.historicalTelemetryPoolMap);
console.log(inputTelemetries);
console.log(outputTelemetries);
console.log(historicalTelemetryDateMap);
console.log(outputTelemetryDateMap);
return outputTelemetryDateMap;
}
addItemToHistoricalTelemetryMap(telemetryMap, item, type, index) {
if (type === 'input') {
telemetryMap.set();
}
}
async getHistoricalData() {
console.log('getHistoricalData');
await this.compositionLoad;
this.setTimeBounds(this.openmct.time.getBounds());
const outputTelemetryMap = await this.getHistoricalInputsByDate();
const formattedOutputTelemetry = this.formatOutputData(outputTelemetryMap);
// const firstObjectKey = this.historicalTelemetryPoolMap.keys().next().value;
// const firstObjectValue = this.historicalTelemetryPoolMap.values().next().value;
// const formattedHistoricalData = this.formatHistoricalData(firstObjectKey, firstObjectValue);
console.log(formattedOutputTelemetry);
// console.log(formattedHistoricalData);
return formattedOutputTelemetry;
}
formatOutputData(outputTelemetryMap) {
const outputTelemetryList = [];
const domainObject = this.conditionSetDomainObject;
outputTelemetryMap.forEach((outputMetadata, timestamp) => {
const { condition, telemetry, value } = outputMetadata;
outputTelemetryList.push({
conditionId: condition.id,
id: domainObject.identifier,
output: value,
utc: timestamp
});
});
return outputTelemetryList;
}
simpleTelemetryList(outputTelemetryMap) {
const outputTelemetryList = [];
outputTelemetryMap.forEach((outputMetadata, timestamp) => {
const { value } = outputMetadata;
outputTelemetryList.push(value);
});
return outputTelemetryList;
}
formatHistoricalData(historicalDataKey, telemetryDetails) {
const formattedData = [];
const { domainObject, historicalTelemetry } = telemetryDetails;
historicalTelemetry.forEach((value) => {
formattedData.push({
id: domainObject.identifier,
output: value.sin,
conditionId: historicalDataKey,
utc: value.utc
});
});
return formattedData;
}
}

View File

@ -243,6 +243,7 @@ export default {
domainObject: {
...this.childObject,
configuration: {
...this.childObject.configuration,
series: [
{
identifier: this.childObject.identifier,

View File

@ -94,7 +94,7 @@ export function ticks(start, stop, count) {
}
export function commonPrefix(a, b) {
const maxLen = Math.min(a.length, b.length);
const maxLen = Math.min(a.length, b?.length);
let breakpoint = 0;
for (let i = 0; i < maxLen; i++) {
if (a[i] !== b[i]) {
@ -110,7 +110,7 @@ export function commonPrefix(a, b) {
}
export function commonSuffix(a, b) {
const maxLen = Math.min(a.length, b.length);
const maxLen = Math.min(a.length, b?.length);
let breakpoint = 0;
for (let i = 0; i <= maxLen; i++) {
if (a[a.length - i] !== b[b.length - i]) {