[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 <jchill2.spam@gmail.com>
This commit is contained in:
Jamie V 2021-07-28 16:46:08 -07:00 committed by GitHub
parent 94c7b2343a
commit ca66898e51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 426 additions and 77 deletions

View File

@ -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;

View File

@ -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);
};
}
}

View File

@ -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);
});
});
});

View File

@ -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));
};
}

View File

@ -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;
});
}

View File

@ -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));
};
};
});

View File

@ -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;
}
}