Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 59 additions & 10 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,14 @@ class Node extends rclnodejs.ShadowNode {

timersReady.forEach((timer) => {
if (timer.isReady()) {
rclnodejs.callTimer(timer.handle);
timer.callback();
let timerInfo;
if (typeof rclnodejs.callTimerWithInfo === 'function') {
timerInfo = rclnodejs.callTimerWithInfo(timer.handle);
timer.callback(timerInfo);
} else {
rclnodejs.callTimer(timer.handle);
timer.callback();
}
}
});

Expand Down Expand Up @@ -628,15 +634,43 @@ class Node extends rclnodejs.ShadowNode {
/**
* Create a Timer.
* @param {bigint} period - The number representing period in nanoseconds.
* @param {function} callback - The callback to be called when timeout.
* @param {Clock} [clock] - The clock which the timer gets time from.
* @param {function} callback - The callback to be called when the timer fires.
* On distros with native support, the callback receives a `TimerInfo` object
* describing the expected and actual call time.
* @param {object|Clock} [optionsOrClock] - Timer options or the clock which the timer gets time from.
* Supported options: `{ autostart?: boolean }`.
* @param {Clock} [clock] - The clock which the timer gets time from when options are provided.
* @return {Timer} - An instance of Timer.
*/
createTimer(period, callback, clock = null) {
if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
clock = null;
} else if (arguments.length === 4) {
clock = arguments[3];
createTimer(period, callback, optionsOrClock = null, clock = null) {
let options = {};

if (optionsOrClock instanceof Clock.Clock) {
clock = optionsOrClock;
} else if (optionsOrClock === null || optionsOrClock === undefined) {
// Keep the 4th argument as the clock when the 3rd argument is omitted or explicitly null.
} else {
if (typeof optionsOrClock !== 'object' || Array.isArray(optionsOrClock)) {
throw new TypeValidationError(
'options',
optionsOrClock,
'object or Clock',
{
nodeName: this.name(),
}
);
}
options = optionsOrClock;
}

if (
arguments.length === 4 &&
clock !== null &&
!(clock instanceof Clock.Clock)
) {
throw new TypeValidationError('clock', clock, 'Clock', {
nodeName: this.name(),
});
}

if (typeof period !== 'bigint') {
Expand All @@ -649,12 +683,27 @@ class Node extends rclnodejs.ShadowNode {
nodeName: this.name(),
});
}
if (
options.autostart !== undefined &&
typeof options.autostart !== 'boolean'
) {
throw new TypeValidationError(
'options.autostart',
options.autostart,
'boolean',
{
nodeName: this.name(),
}
);
}

const timerClock = clock || this._clock;
const autostart = options.autostart ?? true;
let timerHandle = rclnodejs.createTimer(
timerClock.handle,
this.context.handle,
period
period,
autostart
);
let timer = new Timer(timerHandle, period, callback);
debug('Finish creating timer, period = %d.', period);
Expand Down
2 changes: 1 addition & 1 deletion lib/timer.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class Timer {

/**
* Call a timer and starts counting again, retrieves actual and expected call time.
* @return {object} - The timer information.
* @return {{expectedCallTime: bigint, actualCallTime: bigint}} - The timer information.
*/
callTimerWithInfo() {
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
Expand Down
23 changes: 21 additions & 2 deletions src/rcl_timer_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,23 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {

bool lossless;
int64_t period_nsec = info[2].As<Napi::BigInt>().Int64Value(&lossless);
bool autostart = true;
if (info.Length() > 3) {
if (!info[3].IsBoolean()) {
Napi::TypeError::New(env, "Timer autostart must be a boolean")
.ThrowAsJavaScriptException();
return env.Undefined();
}
autostart = info[3].As<Napi::Boolean>().Value();
}
rcl_timer_t* timer =
reinterpret_cast<rcl_timer_t*>(malloc(sizeof(rcl_timer_t)));
*timer = rcl_get_zero_initialized_timer();

#if ROS_VERSION > 2305 // After Iron.
{
rcl_ret_t ret = rcl_timer_init2(timer, clock, context, period_nsec, nullptr,
rcl_get_default_allocator(),
/*autostart=*/true);
rcl_get_default_allocator(), autostart);
if (RCL_RET_OK != ret) {
std::string error_msg = rcl_get_error_string().str;
rcl_reset_error();
Expand All @@ -106,6 +114,17 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
return env.Undefined();
}
if (!autostart) {
rcl_ret_t cancel_ret = rcl_timer_cancel(timer);
if (RCL_RET_OK != cancel_ret) {
std::string error_msg = rcl_get_error_string().str;
rcl_reset_error();
rcl_timer_fini(timer);
free(timer);
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
return env.Undefined();
}
}
}
#endif

Expand Down
87 changes: 87 additions & 0 deletions test/test-timer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const assert = require('assert');
const rclnodejs = require('../index.js');
const rclnodejsNative = require('../lib/native_loader.js');
const DistroUtils = require('../lib/distro.js');
const sinon = require('sinon');

Expand Down Expand Up @@ -186,6 +187,75 @@ describe('rclnodejs Timer class testing', function () {
done();
});

it('node.createTimer supports autostart false', function (done) {
let called = false;
const timer = node.createTimer(
TIMER_INTERVAL,
() => {
called = true;
timer.cancel();
done();
},
{ autostart: false }
);

rclnodejs.spin(node);

setTimeout(() => {
assert.strictEqual(called, false);
timer.reset();
}, 150);
});

it('timer callback receives TimerInfo when available', function (done) {
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
this.skip();
return;
}

const timer = node.createTimer(TIMER_INTERVAL, (info) => {
assert.deepStrictEqual(typeof info.expectedCallTime, 'bigint');
assert.deepStrictEqual(typeof info.actualCallTime, 'bigint');
timer.cancel();
done();
});

rclnodejs.spin(node);
});

it('timer callback preserves zero arguments when TimerInfo is unavailable', function (done) {
const originalFunc = rclnodejsNative.callTimerWithInfo;

try {
Object.defineProperty(rclnodejsNative, 'callTimerWithInfo', {
value: undefined,
configurable: true,
writable: true,
});
} catch (e) {
this.skip();
return;
}

const timer = node.createTimer(TIMER_INTERVAL, function () {
try {
assert.strictEqual(arguments.length, 0);
timer.cancel();
done();
} finally {
try {
Object.defineProperty(rclnodejsNative, 'callTimerWithInfo', {
value: originalFunc,
configurable: true,
writable: true,
});
} catch (e) {}
}
});

rclnodejs.spin(node);
});

it('timer.setOnResetCallback', function (done) {
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
this.skip();
Expand Down Expand Up @@ -340,4 +410,21 @@ describe('rclnodejs Timer class coverage testing', function () {
consoleSpy.restore();
}
});

it('node.createTimer validates autostart option type', function () {
assert.throws(() => {
node.createTimer(TIMER_INTERVAL, () => {}, { autostart: 'false' });
}, /options\.autostart/);
});

it('native createTimer validates autostart argument type', function () {
assert.throws(() => {
rclnodejsNative.createTimer(
node.getClock().handle,
node.context.handle,
TIMER_INTERVAL,
'false'
);
}, /Timer autostart must be a boolean/);
});
});
13 changes: 11 additions & 2 deletions test/types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,20 @@ expectType<boolean>(client.isDestroyed());
expectType<string>(client.loggerName);

// ---- Timer ----
const timerCallback = () => {};
const timerCallback: rclnodejs.TimerRequestCallback = (timerInfo) => {
if (timerInfo) {
expectType<bigint>(timerInfo.expectedCallTime);
expectType<bigint>(timerInfo.actualCallTime);
}
};
expectType<rclnodejs.TimerRequestCallback>(timerCallback);

const timer = node.createTimer(BigInt(100000), timerCallback);
const delayedTimer = node.createTimer(BigInt(100000), timerCallback, {
autostart: false,
});
expectType<rclnodejs.Timer>(timer);
expectType<rclnodejs.Timer>(delayedTimer);
expectType<bigint>(timer.period);
expectType<boolean>(timer.isReady());
expectType<bigint>(timer.timeSinceLastCall());
Expand All @@ -337,7 +346,7 @@ expectType<void>(timer.changeTimerPeriod(BigInt(100000)));
expectType<bigint>(timer.timerPeriod());
expectType<void>(timer.setOnResetCallback((_events: number) => {}));
expectType<void>(timer.clearOnResetCallback());
expectType<object>(timer.callTimerWithInfo());
expectType<rclnodejs.TimerInfo>(timer.callTimerWithInfo());

// ---- Rate ----
const rate = await node.createRate(1);
Expand Down
23 changes: 18 additions & 5 deletions types/node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,24 @@ declare module 'rclnodejs' {
*/
const DEFAULT_OPTIONS: Options;

interface TimerInfo {
expectedCallTime: bigint;
actualCallTime: bigint;
}

interface TimerOptions {
autostart?: boolean;
}

/**
* Callback for receiving periodic interrupts from a Timer.
* Receives timer metadata when the underlying ROS distro exposes it.
*
* @remarks
* See {@link Node.createTimer | Node.createTimer}
* See {@link Timer}
*/
type TimerRequestCallback = () => void;
type TimerRequestCallback = (timerInfo?: TimerInfo) => void;

/**
* Callback indicating parameters are about to be declared or set.
Expand Down Expand Up @@ -313,15 +323,18 @@ declare module 'rclnodejs' {
/**
* Create a Timer.
*
* @param period - Elapsed time between interrupt events (milliseconds).
* @param callback - Called on timeout interrupt.
* @param clock - Optional clock to use for the timer.
* @param period - Elapsed time between interrupt events in nanoseconds.
* @param callback - Called when the timer fires. Receives a `TimerInfo` argument when available.
* @param optionsOrClock - Optional timer options or clock to use for the timer.
* Supports `{ autostart?: boolean }` when an options object is provided.
* @param clock - Optional clock to use for the timer when options are provided.
* @returns New instance of Timer.
*/
createTimer(
period: bigint,
callback: TimerRequestCallback,
clock?: Clock
optionsOrClock?: TimerOptions | Clock | null,
clock?: Clock | null
): Timer;

/**
Expand Down
5 changes: 3 additions & 2 deletions types/timer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ declare module 'rclnodejs' {

/**
* Call a timer and starts counting again, retrieves actual and expected call time.
* @return - The timer information.
*
* @return The timer information with expected and actual call timestamps.
*/
callTimerWithInfo(): object;
callTimerWithInfo(): TimerInfo;
}
}
Loading