From ca66898e51280b7618926cc42eebe36c0e2524f2 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Wed, 28 Jul 2021 16:46:08 -0700 Subject: [PATCH] [Plugin] Remote Clock based off Telemetry (#3998) * Remote-clock plugin * Added a default clock class that can be extended by other classes * Updated local clock to use the parent default clock class * added a period check to make sure its been a certain amount of time before emitting Co-authored-by: John Hill --- src/plugins/plugins.js | 3 + src/plugins/remoteClock/RemoteClock.js | 132 ++++++++++++++++++ src/plugins/remoteClock/RemoteClockSpec.js | 149 +++++++++++++++++++++ src/plugins/remoteClock/plugin.js | 32 +++++ src/plugins/utcTimeSystem/LocalClock.js | 96 +++---------- src/plugins/utcTimeSystem/plugin.js | 2 +- src/utils/clock/DefaultClock.js | 89 ++++++++++++ 7 files changed, 426 insertions(+), 77 deletions(-) create mode 100644 src/plugins/remoteClock/RemoteClock.js create mode 100644 src/plugins/remoteClock/RemoteClockSpec.js create mode 100644 src/plugins/remoteClock/plugin.js create mode 100644 src/utils/clock/DefaultClock.js diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 72ac7063cf..b7a383dae7 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -23,6 +23,7 @@ define([ 'lodash', './utcTimeSystem/plugin', + './remoteClock/plugin', './localTimeSystem/plugin', './ISOTimeFormat/plugin', '../../example/generator/plugin', @@ -69,6 +70,7 @@ define([ ], function ( _, UTCTimeSystem, + RemoteClock, LocalTimeSystem, ISOTimeFormat, GeneratorPlugin, @@ -129,6 +131,7 @@ define([ plugins.UTCTimeSystem = UTCTimeSystem; plugins.LocalTimeSystem = LocalTimeSystem; + plugins.RemoteClock = RemoteClock.default; plugins.ImportExport = ImportExport; diff --git a/src/plugins/remoteClock/RemoteClock.js b/src/plugins/remoteClock/RemoteClock.js new file mode 100644 index 0000000000..96aa336248 --- /dev/null +++ b/src/plugins/remoteClock/RemoteClock.js @@ -0,0 +1,132 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2021, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +import DefaultClock from '../../utils/clock/DefaultClock'; + +/** + * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the + * application based on a time providing telemetry domainObject. + * + * @param {openmct} Object Instance of OpenMCT + * @param {module:openmct.ObjectAPI~Identifier} identifier An object identifier for + * the time providing telemetry domainObject + * @constructor + */ + +export default class RemoteClock extends DefaultClock { + constructor(openmct, identifier) { + super(); + + this.key = 'remote-clock'; + + this.openmct = openmct; + this.identifier = identifier; + + this.name = 'Remote Clock'; + this.description = "Provides telemetry based timestamps from a configurable source."; + + this.timeTelemetryObject = undefined; + this.parseTime = undefined; + this.metadata = undefined; + + this.lastTick = 0; + + this._processDatum = this._processDatum.bind(this); + } + + start() { + this.openmct.time.on('timeSystem', this._timeSystemChange); + this.openmct.objects.get(this.identifier).then((domainObject) => { + this.timeTelemetryObject = domainObject; + this.metadata = this.openmct.telemetry.getMetadata(domainObject); + this._timeSystemChange(); + this._requestLatest(); + this._subscribe(); + }).catch((error) => { + throw new Error(error); + }); + } + + stop() { + this.openmct.time.off('timeSystem', this._timeSystemChange); + if (this._unsubscribe) { + this._unsubscribe(); + } + + this.removeAllListeners(); + } + + /** + * Will start a subscription to the timeTelemetryObject as well + * handle the unsubscribe callback + * + * @private + */ + _subscribe() { + this._unsubscribe = this.openmct.telemetry.subscribe( + this.timeTelemetryObject, + this._processDatum + ); + } + + /** + * Will request the latest data for the timeTelemetryObject + * + * @private + */ + _requestLatest() { + this.openmct.telemetry.request(this.timeTelemetryObject, { + size: 1, + strategy: 'latest' + }).then(data => { + this._processDatum(data[data.length - 1]); + }); + } + + /** + * Function to parse the datum from the timeTelemetryObject as well + * as check if it's valid, calls "tick" + * + * @private + */ + _processDatum(datum) { + let time = this.parseTime(datum); + + if (time > this.lastTick) { + this.tick(time); + } + } + + /** + * Callback function for timeSystem change events + * + * @private + */ + _timeSystemChange() { + let timeSystem = this.openmct.time.timeSystem(); + let timeKey = timeSystem.key; + let metadataValue = this.metadata.value(timeKey); + let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + this.parseTime = (datum) => { + return timeFormatter.parse(datum); + }; + } +} diff --git a/src/plugins/remoteClock/RemoteClockSpec.js b/src/plugins/remoteClock/RemoteClockSpec.js new file mode 100644 index 0000000000..83adc18544 --- /dev/null +++ b/src/plugins/remoteClock/RemoteClockSpec.js @@ -0,0 +1,149 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import { + createOpenMct, + resetApplicationState +} from 'utils/testing'; + +const REMOTE_CLOCK_KEY = 'remote-clock'; +const TIME_TELEMETRY_ID = { + namespace: 'remote', + key: 'telemetry' +}; +const TIME_VALUE = 12345; +const REQ_OPTIONS = { + size: 1, + strategy: 'latest' +}; +const OFFSET_START = -10; +const OFFSET_END = 1; + +describe("the RemoteClock plugin", () => { + let openmct; + let object = { + name: 'remote-telemetry', + identifier: TIME_TELEMETRY_ID + }; + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('once installed', () => { + let remoteClock; + let boundsCallback; + let metadataValue = { some: 'value' }; + let timeSystem = { key: 'utc' }; + let metadata = { + value: () => metadataValue + }; + let reqDatum = { + key: TIME_VALUE + }; + + let formatter = { + parse: (datum) => datum.key + }; + + beforeEach((done) => { + openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID)); + + let clocks = openmct.time.getAllClocks(); + remoteClock = clocks.filter(clock => clock.key === REMOTE_CLOCK_KEY)[0]; + + boundsCallback = jasmine.createSpy("boundsCallback"); + openmct.time.on('bounds', boundsCallback); + + spyOn(remoteClock, '_timeSystemChange').and.callThrough(); + spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter); + spyOn(openmct.telemetry, 'subscribe').and.callThrough(); + spyOn(openmct.time, 'on').and.callThrough(); + spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem); + spyOn(metadata, 'value').and.callThrough(); + + let requestPromiseResolve; + let requestPromise = new Promise((resolve) => { + requestPromiseResolve = resolve; + }); + spyOn(openmct.telemetry, 'request').and.callFake(() => { + requestPromiseResolve([reqDatum]); + + return requestPromise; + }); + + let objectPromiseResolve; + let objectPromise = new Promise((resolve) => { + objectPromiseResolve = resolve; + }); + spyOn(openmct.objects, 'get').and.callFake(() => { + objectPromiseResolve(object); + + return objectPromise; + }); + + openmct.time.clock(REMOTE_CLOCK_KEY, { + start: OFFSET_START, + end: OFFSET_END + }); + + Promise.all([objectPromiseResolve, requestPromise]) + .then(done) + .catch(done); + }); + + it('is available and sets up initial values and listeners', () => { + expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY); + expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID); + expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange); + expect(remoteClock._timeSystemChange).toHaveBeenCalled(); + }); + + it('will request/store the object based on the identifier passed in', () => { + expect(remoteClock.timeTelemetryObject).toEqual(object); + }); + + it('will request metadata and set up formatters', () => { + expect(remoteClock.metadata).toEqual(metadata); + expect(metadata.value).toHaveBeenCalled(); + expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue); + }); + + it('will request the latest datum for the object it received and process the datum returned', () => { + expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS); + expect(boundsCallback).toHaveBeenCalledWith({ start: TIME_VALUE + OFFSET_START, end: TIME_VALUE + OFFSET_END }, true); + }); + + it('will set up subscriptions correctly', () => { + expect(remoteClock._unsubscribe).toBeDefined(); + expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum); + }); + }); + +}); diff --git a/src/plugins/remoteClock/plugin.js b/src/plugins/remoteClock/plugin.js new file mode 100644 index 0000000000..0ff90f1f62 --- /dev/null +++ b/src/plugins/remoteClock/plugin.js @@ -0,0 +1,32 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2021, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import RemoteClock from "./RemoteClock"; +/** + * Install a clock that uses a configurable telemetry endpoint. + */ + +export default function (identifier) { + return function (openmct) { + openmct.time.addClock(new RemoteClock(openmct, identifier)); + }; +} diff --git a/src/plugins/utcTimeSystem/LocalClock.js b/src/plugins/utcTimeSystem/LocalClock.js index fb8269cf16..d71262e870 100644 --- a/src/plugins/utcTimeSystem/LocalClock.js +++ b/src/plugins/utcTimeSystem/LocalClock.js @@ -20,22 +20,20 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define(['EventEmitter'], function (EventEmitter) { - /** - * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the - * application based on UTC time values provided by a ticking local clock, - * with the periodicity specified. - * @param {number} period The periodicity with which the clock should tick - * @constructor - */ - function LocalClock(period) { - EventEmitter.call(this); +import DefaultClock from "../../utils/clock/DefaultClock"; +/** + * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the + * application based on UTC time values provided by a ticking local clock, + * with the periodicity specified. + * @param {number} period The periodicity with which the clock should tick + * @constructor + */ + +export default class LocalClock extends DefaultClock { + constructor(period = 100) { + super(); - /* - Metadata fields - */ this.key = 'local'; - this.cssClass = 'icon-clock'; this.name = 'Local Clock'; this.description = "Provides UTC timestamps every second from the local system clock."; @@ -44,75 +42,21 @@ define(['EventEmitter'], function (EventEmitter) { this.lastTick = Date.now(); } - LocalClock.prototype = Object.create(EventEmitter.prototype); - - /** - * @private - */ - LocalClock.prototype.start = function () { + start() { this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); - }; + } - /** - * @private - */ - LocalClock.prototype.stop = function () { + stop() { if (this.timeoutHandle) { clearTimeout(this.timeoutHandle); this.timeoutHandle = undefined; } - }; + } - /** - * @private - */ - LocalClock.prototype.tick = function () { + tick() { const now = Date.now(); - this.emit("tick", now); - this.lastTick = now; + super.tick(now); this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); - }; + } - /** - * Register a listener for the local clock. When it ticks, the local - * clock will provide the current local system time - * - * @param listener - * @returns {function} a function for deregistering the provided listener - */ - LocalClock.prototype.on = function (event) { - const result = EventEmitter.prototype.on.apply(this, arguments); - - if (this.listeners(event).length === 1) { - this.start(); - } - - return result; - }; - - /** - * Register a listener for the local clock. When it ticks, the local - * clock will provide the current local system time - * - * @param listener - * @returns {function} a function for deregistering the provided listener - */ - LocalClock.prototype.off = function (event) { - const result = EventEmitter.prototype.off.apply(this, arguments); - - if (this.listeners(event).length === 0) { - this.stop(); - } - - return result; - }; - - /** - * @returns {number} The last value provided for a clock tick - */ - LocalClock.prototype.currentValue = function () { - return this.lastTick; - }; - - return LocalClock; -}); +} diff --git a/src/plugins/utcTimeSystem/plugin.js b/src/plugins/utcTimeSystem/plugin.js index 44dcef5901..2f2a2ae506 100644 --- a/src/plugins/utcTimeSystem/plugin.js +++ b/src/plugins/utcTimeSystem/plugin.js @@ -35,7 +35,7 @@ define([ return function (openmct) { const timeSystem = new UTCTimeSystem(); openmct.time.addTimeSystem(timeSystem); - openmct.time.addClock(new LocalClock(100)); + openmct.time.addClock(new LocalClock.default(100)); }; }; }); diff --git a/src/utils/clock/DefaultClock.js b/src/utils/clock/DefaultClock.js new file mode 100644 index 0000000000..40b6cbbec1 --- /dev/null +++ b/src/utils/clock/DefaultClock.js @@ -0,0 +1,89 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2021, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +import EventEmitter from 'EventEmitter'; + +/** + * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the + * application based on values provided by a ticking clock, + * with the periodicity specified (optionally). + * @param {number} period The periodicity with which the clock should tick + * @constructor + */ + +export default class DefaultClock extends EventEmitter { + constructor() { + super(); + + this.key = 'clock'; + + this.cssClass = 'icon-clock'; + this.name = 'Clock'; + this.description = "A default clock for openmct."; + } + + tick(tickValue) { + this.emit("tick", tickValue); + this.lastTick = tickValue; + } + + /** + * Register a listener for the clock. When it ticks, the + * clock will provide the time from the configured endpoint + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + on(event) { + let result = super.on.apply(this, arguments); + + if (this.listeners(event).length === 1) { + this.start(); + } + + return result; + } + + /** + * Register a listener for the clock. When it ticks, the + * clock will provide the current local system time + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + off(event) { + let result = super.off.apply(this, arguments); + + if (this.listeners(event).length === 0) { + this.stop(); + } + + return result; + } + + /** + * @returns {number} The last value provided for a clock tick + */ + currentValue() { + return this.lastTick; + } + +}