// Copyright (c) 2018 Intel Corporation. All rights reserved.
//
// 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.
'use strict';
const rclnodejs = require('./native_loader.js');
const Time = require('./time.js');
const ClockType = require('./clock_type.js');
const ClockChange = require('./clock_change.js');
const Context = require('./context.js');
const ClockEvent = require('./clock_event.js');
const { TypeValidationError } = require('./errors.js');
/**
* @class - Class representing a Clock in ROS
*/
class Clock {
/**
* Create a Clock.
* @param {ClockType} [clockType=Clock.ClockType.SYSTEM_TIME] - The type of the clock to be created.
*/
constructor(clockType = ClockType.SYSTEM_TIME) {
this._clockType = clockType;
this._handle = rclnodejs.createClock(this._clockType);
}
/**
* @ignore
*/
get handle() {
return this._handle;
}
/**
* Get ClockType of this Clock object.
* @return {ClockType} Return the type of the clock.
*/
get clockType() {
return this._clockType;
}
/**
* Add a clock callback.
* @param {object} callbackObject - The object containing callback methods.
* @param {Function} [callbackObject._pre_callback] - Optional callback invoked before a time jump. Takes no arguments.
* @param {Function} [callbackObject._post_callback] - Optional callback invoked after a time jump.
* Receives a jumpInfo object with properties:
* - clock_change {number}: Type of clock change that occurred.
* - delta {bigint}: Time delta in nanoseconds.
* @param {boolean} onClockChange - Whether to call the callback on clock change.
* @param {bigint} minForward - Minimum forward jump in nanoseconds to trigger the callback.
* @param {bigint} minBackward - Minimum backward jump in nanoseconds to trigger the callback.
*/
addClockCallback(callbackObject, onClockChange, minForward, minBackward) {
if (typeof callbackObject !== 'object' || callbackObject === null) {
throw new TypeValidationError(
'callbackObject',
callbackObject,
'object',
{
entityType: 'clock',
}
);
}
if (typeof onClockChange !== 'boolean') {
throw new TypeValidationError('onClockChange', onClockChange, 'boolean', {
entityType: 'clock',
});
}
if (typeof minForward !== 'bigint') {
throw new TypeValidationError('minForward', minForward, 'bigint', {
entityType: 'clock',
});
}
if (typeof minBackward !== 'bigint') {
throw new TypeValidationError('minBackward', minBackward, 'bigint', {
entityType: 'clock',
});
}
rclnodejs.clockAddJumpCallback(
this._handle,
callbackObject,
onClockChange,
minForward,
minBackward
);
}
/**
* Remove a clock callback.
* @param {object} callbackObject - The callback object that was previously registered with addClockCallback().
*/
removeClockCallback(callbackObject) {
if (typeof callbackObject !== 'object' || callbackObject === null) {
throw new TypeValidationError(
'callbackObject',
callbackObject,
'object',
{
entityType: 'clock',
}
);
}
rclnodejs.clockRemoveJumpCallback(this._handle, callbackObject);
}
/**
* Return the current time.
* @return {Time} Return the current time.
*/
now() {
const nowInNanosec = rclnodejs.clockGetNow(this._handle);
return new Time(0n, nowInNanosec, this._clockType);
}
/**
* Sleep until a specific time is reached on this Clock.
*
* When using a ROSClock, this may sleep forever if the TimeSource is misconfigured
* and the context is never shut down. ROS time being activated or deactivated causes
* this function to cease sleeping and return false.
*
* @param {Time} until - Time at which this function should stop sleeping.
* @param {Context} [context=null] - Context whose validity will be checked while sleeping.
* If context is null, then the default context is used. If the context is found to be
* shut down before or by the time the wait completes, the returned promise resolves to false.
* @return {Promise<boolean>} Promise that resolves to true if 'until' was reached without
* detecting a time jump or a shut down context, or false otherwise (for example, if a time
* jump occurred or the context is no longer valid when the wait completes).
* @throws {TypeError} if until is specified for a different type of clock than this one.
* @throws {Error} if context has not been initialized or is shutdown.
*/
async sleepUntil(until, context = null) {
if (!(until instanceof Time)) {
throw new TypeValidationError('until', until, 'Time', {
entityType: 'clock',
});
}
if (!context) {
context = Context.defaultContext();
}
if (!context.isValid()) {
throw new Error('Context is not initialized or has been shut down');
}
if (until.clockType !== this._clockType) {
throw new TypeError(
"until's clock type does not match this clock's type"
);
}
const event = new ClockEvent();
let timeSourceChanged = false;
// Callback to wake when time jumps and is past target time
const callbackObject = {
_pre_callback: () => {
// Optional pre-callback - no action needed
},
_post_callback: (jumpInfo) => {
// ROS time being activated or deactivated changes the epoch,
// so sleep time loses its meaning
timeSourceChanged =
timeSourceChanged ||
jumpInfo.clock_change === ClockChange.ROS_TIME_ACTIVATED ||
jumpInfo.clock_change === ClockChange.ROS_TIME_DEACTIVATED;
if (timeSourceChanged || this.now().nanoseconds >= until.nanoseconds) {
event.set();
}
},
};
// Setup jump callback with minimal forward threshold
this.addClockCallback(
callbackObject,
true, // onClockChange
1n, // minForward (1 nanosecond - any forward jump triggers)
0n // minBackward (0 disables backward threshold)
);
try {
// Wait based on clock type
if (this._clockType === ClockType.SYSTEM_TIME) {
await event.waitUntilSystem(this, until.nanoseconds);
} else if (this._clockType === ClockType.STEADY_TIME) {
await event.waitUntilSteady(this, until.nanoseconds);
} else if (this._clockType === ClockType.ROS_TIME) {
await event.waitUntilRos(this, until.nanoseconds);
}
} finally {
// Always clean up callback
this.removeClockCallback(callbackObject);
}
if (!context.isValid() || timeSourceChanged) {
return false;
}
return this.now().nanoseconds >= until.nanoseconds;
}
/**
* Sleep for a specified duration.
*
* Equivalent to: clock.sleepUntil(clock.now() + duration, context)
*
* When using a ROSClock, this may sleep forever if the TimeSource is misconfigured
* and the context is never shut down. ROS time being activated or deactivated causes
* this function to cease sleeping and return false.
*
* @param {Duration} duration - Duration of time to sleep for.
* @param {Context} [context=null] - Context which when shut down will cause this sleep to wake early.
* If context is null, then the default context is used.
* @return {Promise<boolean>} Promise that resolves to true if the full duration was slept,
* or false if it woke for another reason.
* @throws {Error} if context has not been initialized or is shutdown.
*/
async sleepFor(duration, context = null) {
const until = this.now().add(duration);
return this.sleepUntil(until, context);
}
}
/**
* @class - Class representing a ROSClock in ROS
*/
class ROSClock extends Clock {
/**
* Create a ROSClock.
*/
constructor() {
super(ClockType.ROS_TIME);
}
/**
* Return status that whether the ROS time is active.
* @name ROSClock#get:isRosTimeActive
* @function
* @return {boolean} Return true if the time is active, otherwise return false.
*/
get isRosTimeActive() {
return rclnodejs.getRosTimeOverrideIsEnabled(this._handle);
}
/**
* Set the status of ROS time.
* @param {boolean} enabled - Set the ROS time to be active.
* @name ROSClock#set:isRosTimeActive
* @function
* @return {undefined}
*/
set isRosTimeActive(enabled) {
rclnodejs.setRosTimeOverrideIsEnabled(this._handle, enabled);
}
/**
* Set the status of ROS time.
* @param {Time} time - The time to be set override.
* @name ROSClock#set:rosTimeOverride
* @function
* @return {undefined}
*/
set rosTimeOverride(time) {
if (!(time instanceof Time)) {
throw new TypeValidationError('time', time, 'Time', {
entityType: 'clock',
});
}
rclnodejs.setRosTimeOverride(this._handle, time._handle);
}
}
Clock.ClockType = ClockType;
module.exports = { Clock, ROSClock };