Skip to content

Commit 6b6bf4b

Browse files
committed
Add timer autostart and TimerInfo callback support
1 parent 5b88d78 commit 6b6bf4b

7 files changed

Lines changed: 149 additions & 22 deletions

File tree

lib/node.js

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,13 @@ class Node extends rclnodejs.ShadowNode {
256256

257257
timersReady.forEach((timer) => {
258258
if (timer.isReady()) {
259-
rclnodejs.callTimer(timer.handle);
260-
timer.callback();
259+
let timerInfo;
260+
if (typeof rclnodejs.callTimerWithInfo === 'function') {
261+
timerInfo = rclnodejs.callTimerWithInfo(timer.handle);
262+
} else {
263+
rclnodejs.callTimer(timer.handle);
264+
}
265+
timer.callback(timerInfo);
261266
}
262267
});
263268

@@ -628,15 +633,43 @@ class Node extends rclnodejs.ShadowNode {
628633
/**
629634
* Create a Timer.
630635
* @param {bigint} period - The number representing period in nanoseconds.
631-
* @param {function} callback - The callback to be called when timeout.
632-
* @param {Clock} [clock] - The clock which the timer gets time from.
636+
* @param {function} callback - The callback to be called when the timer fires.
637+
* On distros with native support, the callback receives a `TimerInfo` object
638+
* describing the expected and actual call time.
639+
* @param {object|Clock} [optionsOrClock] - Timer options or the clock which the timer gets time from.
640+
* Supported options: `{ autostart?: boolean }`.
641+
* @param {Clock} [clock] - The clock which the timer gets time from when options are provided.
633642
* @return {Timer} - An instance of Timer.
634643
*/
635-
createTimer(period, callback, clock = null) {
636-
if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
637-
clock = null;
638-
} else if (arguments.length === 4) {
639-
clock = arguments[3];
644+
createTimer(period, callback, optionsOrClock = null, clock = null) {
645+
let options = {};
646+
647+
if (optionsOrClock instanceof Clock.Clock) {
648+
clock = optionsOrClock;
649+
} else if (optionsOrClock === null || optionsOrClock === undefined) {
650+
// Keep the 4th argument as the clock when the 3rd argument is omitted or explicitly null.
651+
} else {
652+
if (typeof optionsOrClock !== 'object' || Array.isArray(optionsOrClock)) {
653+
throw new TypeValidationError(
654+
'options',
655+
optionsOrClock,
656+
'object or Clock',
657+
{
658+
nodeName: this.name(),
659+
}
660+
);
661+
}
662+
options = optionsOrClock;
663+
}
664+
665+
if (
666+
arguments.length === 4 &&
667+
clock !== null &&
668+
!(clock instanceof Clock.Clock)
669+
) {
670+
throw new TypeValidationError('clock', clock, 'Clock', {
671+
nodeName: this.name(),
672+
});
640673
}
641674

642675
if (typeof period !== 'bigint') {
@@ -649,12 +682,27 @@ class Node extends rclnodejs.ShadowNode {
649682
nodeName: this.name(),
650683
});
651684
}
685+
if (
686+
options.autostart !== undefined &&
687+
typeof options.autostart !== 'boolean'
688+
) {
689+
throw new TypeValidationError(
690+
'options.autostart',
691+
options.autostart,
692+
'boolean',
693+
{
694+
nodeName: this.name(),
695+
}
696+
);
697+
}
652698

653699
const timerClock = clock || this._clock;
700+
const autostart = options.autostart ?? true;
654701
let timerHandle = rclnodejs.createTimer(
655702
timerClock.handle,
656703
this.context.handle,
657-
period
704+
period,
705+
autostart
658706
);
659707
let timer = new Timer(timerHandle, period, callback);
660708
debug('Finish creating timer, period = %d.', period);

lib/timer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ class Timer {
150150

151151
/**
152152
* Call a timer and starts counting again, retrieves actual and expected call time.
153-
* @return {object} - The timer information.
153+
* @return {{expectedCallTime: bigint, actualCallTime: bigint}} - The timer information.
154154
*/
155155
callTimerWithInfo() {
156156
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {

src/rcl_timer_bindings.cpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,18 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
7878

7979
bool lossless;
8080
int64_t period_nsec = info[2].As<Napi::BigInt>().Int64Value(&lossless);
81+
bool autostart = true;
82+
if (info.Length() > 3 && info[3].IsBoolean()) {
83+
autostart = info[3].As<Napi::Boolean>().Value();
84+
}
8185
rcl_timer_t* timer =
8286
reinterpret_cast<rcl_timer_t*>(malloc(sizeof(rcl_timer_t)));
8387
*timer = rcl_get_zero_initialized_timer();
8488

8589
#if ROS_VERSION > 2305 // After Iron.
8690
{
8791
rcl_ret_t ret = rcl_timer_init2(timer, clock, context, period_nsec, nullptr,
88-
rcl_get_default_allocator(),
89-
/*autostart=*/true);
92+
rcl_get_default_allocator(), autostart);
9093
if (RCL_RET_OK != ret) {
9194
std::string error_msg = rcl_get_error_string().str;
9295
rcl_reset_error();
@@ -106,6 +109,17 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
106109
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
107110
return env.Undefined();
108111
}
112+
if (!autostart) {
113+
rcl_ret_t cancel_ret = rcl_timer_cancel(timer);
114+
if (RCL_RET_OK != cancel_ret) {
115+
std::string error_msg = rcl_get_error_string().str;
116+
rcl_reset_error();
117+
rcl_timer_fini(timer);
118+
free(timer);
119+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
120+
return env.Undefined();
121+
}
122+
}
109123
}
110124
#endif
111125

test/test-timer.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,42 @@ describe('rclnodejs Timer class testing', function () {
186186
done();
187187
});
188188

189+
it('node.createTimer supports autostart false', function (done) {
190+
let called = false;
191+
const timer = node.createTimer(
192+
TIMER_INTERVAL,
193+
() => {
194+
called = true;
195+
timer.cancel();
196+
done();
197+
},
198+
{ autostart: false }
199+
);
200+
201+
rclnodejs.spin(node);
202+
203+
setTimeout(() => {
204+
assert.strictEqual(called, false);
205+
timer.reset();
206+
}, 150);
207+
});
208+
209+
it('timer callback receives TimerInfo when available', function (done) {
210+
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
211+
this.skip();
212+
return;
213+
}
214+
215+
const timer = node.createTimer(TIMER_INTERVAL, (info) => {
216+
assert.deepStrictEqual(typeof info.expectedCallTime, 'bigint');
217+
assert.deepStrictEqual(typeof info.actualCallTime, 'bigint');
218+
timer.cancel();
219+
done();
220+
});
221+
222+
rclnodejs.spin(node);
223+
});
224+
189225
it('timer.setOnResetCallback', function (done) {
190226
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
191227
this.skip();
@@ -340,4 +376,10 @@ describe('rclnodejs Timer class coverage testing', function () {
340376
consoleSpy.restore();
341377
}
342378
});
379+
380+
it('node.createTimer validates autostart option type', function () {
381+
assert.throws(() => {
382+
node.createTimer(TIMER_INTERVAL, () => {}, { autostart: 'false' });
383+
}, /options\.autostart/);
384+
});
343385
});

test/types/index.test-d.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,20 @@ expectType<boolean>(client.isDestroyed());
321321
expectType<string>(client.loggerName);
322322

323323
// ---- Timer ----
324-
const timerCallback = () => {};
324+
const timerCallback: rclnodejs.TimerRequestCallback = (timerInfo) => {
325+
if (timerInfo) {
326+
expectType<bigint>(timerInfo.expectedCallTime);
327+
expectType<bigint>(timerInfo.actualCallTime);
328+
}
329+
};
325330
expectType<rclnodejs.TimerRequestCallback>(timerCallback);
326331

327332
const timer = node.createTimer(BigInt(100000), timerCallback);
333+
const delayedTimer = node.createTimer(BigInt(100000), timerCallback, {
334+
autostart: false,
335+
});
328336
expectType<rclnodejs.Timer>(timer);
337+
expectType<rclnodejs.Timer>(delayedTimer);
329338
expectType<bigint>(timer.period);
330339
expectType<boolean>(timer.isReady());
331340
expectType<bigint>(timer.timeSinceLastCall());
@@ -337,7 +346,7 @@ expectType<void>(timer.changeTimerPeriod(BigInt(100000)));
337346
expectType<bigint>(timer.timerPeriod());
338347
expectType<void>(timer.setOnResetCallback((_events: number) => {}));
339348
expectType<void>(timer.clearOnResetCallback());
340-
expectType<object>(timer.callTimerWithInfo());
349+
expectType<rclnodejs.TimerInfo>(timer.callTimerWithInfo());
341350

342351
// ---- Rate ----
343352
const rate = await node.createRate(1);

types/node.d.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,24 @@ declare module 'rclnodejs' {
104104
*/
105105
const DEFAULT_OPTIONS: Options;
106106

107+
interface TimerInfo {
108+
expectedCallTime: bigint;
109+
actualCallTime: bigint;
110+
}
111+
112+
interface TimerOptions {
113+
autostart?: boolean;
114+
}
115+
107116
/**
108117
* Callback for receiving periodic interrupts from a Timer.
118+
* Receives timer metadata when the underlying ROS distro exposes it.
109119
*
110120
* @remarks
111121
* See {@link Node.createTimer | Node.createTimer}
112122
* See {@link Timer}
113123
*/
114-
type TimerRequestCallback = () => void;
124+
type TimerRequestCallback = (timerInfo?: TimerInfo) => void;
115125

116126
/**
117127
* Callback indicating parameters are about to be declared or set.
@@ -313,15 +323,18 @@ declare module 'rclnodejs' {
313323
/**
314324
* Create a Timer.
315325
*
316-
* @param period - Elapsed time between interrupt events (milliseconds).
317-
* @param callback - Called on timeout interrupt.
318-
* @param clock - Optional clock to use for the timer.
326+
* @param period - Elapsed time between interrupt events in nanoseconds.
327+
* @param callback - Called when the timer fires. Receives a `TimerInfo` argument when available.
328+
* @param optionsOrClock - Optional timer options or clock to use for the timer.
329+
* Supports `{ autostart?: boolean }` when an options object is provided.
330+
* @param clock - Optional clock to use for the timer when options are provided.
319331
* @returns New instance of Timer.
320332
*/
321333
createTimer(
322334
period: bigint,
323335
callback: TimerRequestCallback,
324-
clock?: Clock
336+
optionsOrClock?: TimerOptions | Clock | null,
337+
clock?: Clock | null
325338
): Timer;
326339

327340
/**

types/timer.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ declare module 'rclnodejs' {
7878

7979
/**
8080
* Call a timer and starts counting again, retrieves actual and expected call time.
81-
* @return - The timer information.
81+
*
82+
* @return The timer information with expected and actual call timestamps.
8283
*/
83-
callTimerWithInfo(): object;
84+
callTimerWithInfo(): TimerInfo;
8485
}
8586
}

0 commit comments

Comments
 (0)