Skip to content

Commit e6e95c1

Browse files
authored
Add rcl_timer_set_on_reset_callback() support (#1348)
This PR adds support for the ROS 2 `rcl_timer_set_on_reset_callback()` API to rclnodejs, enabling users to register callbacks that are invoked when a timer is reset. The implementation provides both JavaScript and TypeScript APIs with corresponding native C++ bindings. **Key Changes:** - Added `setOnResetCallback()` and `clearOnResetCallback()` methods to the Timer class - Implemented native C++ bindings using ThreadSafeFunction for cross-thread callback invocation - Added comprehensive test coverage for the new callback functionality Fix: #1330
1 parent a8e289c commit e6e95c1

File tree

5 files changed

+216
-0
lines changed

5 files changed

+216
-0
lines changed

lib/timer.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,35 @@ class Timer {
105105
return rclnodejs.getTimerPeriod(this._handle);
106106
}
107107

108+
/**
109+
* Set the on reset callback.
110+
* @param {function} callback - The callback to be called when the timer is reset.
111+
* @return {undefined}
112+
*/
113+
setOnResetCallback(callback) {
114+
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
115+
console.warn(
116+
'setOnResetCallback is not supported by this version of ROS 2'
117+
);
118+
return;
119+
}
120+
rclnodejs.setTimerOnResetCallback(this._handle, callback);
121+
}
122+
123+
/**
124+
* Clear the on reset callback.
125+
* @return {undefined}
126+
*/
127+
clearOnResetCallback() {
128+
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
129+
console.warn(
130+
'clearOnResetCallback is not supported by this version of ROS 2'
131+
);
132+
return;
133+
}
134+
rclnodejs.clearTimerOnResetCallback(this._handle);
135+
}
136+
108137
/**
109138
* Call a timer and starts counting again, retrieves actual and expected call time.
110139
* @return {object} - The timer information.

src/rcl_timer_bindings.cpp

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,48 @@
1717
#include <rcl/error_handling.h>
1818
#include <rcl/rcl.h>
1919

20+
#include <memory>
21+
#include <mutex>
22+
#include <unordered_map>
23+
2024
#include "macros.h"
2125
#include "rcl_handle.h"
2226
#include "rcl_utilities.h"
2327

2428
namespace rclnodejs {
2529

30+
struct TimerContext {
31+
Napi::ThreadSafeFunction on_reset_callback;
32+
};
33+
34+
static std::unordered_map<rcl_timer_t*, std::shared_ptr<TimerContext>>
35+
g_timer_contexts;
36+
static std::mutex g_timer_contexts_mutex;
37+
38+
void TimerOnResetCallbackTrampoline(const void* user_data,
39+
size_t number_of_events) {
40+
const rcl_timer_t* timer = static_cast<const rcl_timer_t*>(user_data);
41+
std::shared_ptr<TimerContext> context;
42+
43+
{
44+
std::lock_guard<std::mutex> lock(g_timer_contexts_mutex);
45+
auto it = g_timer_contexts.find(const_cast<rcl_timer_t*>(timer));
46+
if (it != g_timer_contexts.end()) {
47+
context = it->second;
48+
}
49+
}
50+
51+
if (context) {
52+
auto callback = [](Napi::Env env, Napi::Function js_callback,
53+
size_t* events) {
54+
js_callback.Call({Napi::Number::New(env, *events)});
55+
delete events;
56+
};
57+
size_t* events_ptr = new size_t(number_of_events);
58+
context->on_reset_callback.BlockingCall(events_ptr, callback);
59+
}
60+
}
61+
2662
Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
2763
Napi::Env env = info.Env();
2864

@@ -61,6 +97,27 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
6197
auto js_obj =
6298
RclHandle::NewInstance(env, timer, clock_handle, [env](void* ptr) {
6399
rcl_timer_t* timer = reinterpret_cast<rcl_timer_t*>(ptr);
100+
101+
#if ROS_VERSION > 2205
102+
// Clear the callback first to prevent any new callbacks from being
103+
// triggered
104+
rcl_timer_set_on_reset_callback(timer, nullptr, nullptr);
105+
#endif
106+
107+
std::shared_ptr<TimerContext> context;
108+
{
109+
std::lock_guard<std::mutex> lock(g_timer_contexts_mutex);
110+
auto it = g_timer_contexts.find(timer);
111+
if (it != g_timer_contexts.end()) {
112+
context = it->second;
113+
g_timer_contexts.erase(it);
114+
}
115+
}
116+
117+
if (context) {
118+
context->on_reset_callback.Release();
119+
}
120+
64121
rcl_ret_t ret = rcl_timer_fini(timer);
65122
free(ptr);
66123
THROW_ERROR_IF_NOT_EQUAL_NO_RETURN(RCL_RET_OK, ret,
@@ -215,6 +272,60 @@ Napi::Value CallTimerWithInfo(const Napi::CallbackInfo& info) {
215272
Napi::BigInt::New(env, call_info.actual_call_time));
216273
return timer_info;
217274
}
275+
276+
Napi::Value SetTimerOnResetCallback(const Napi::CallbackInfo& info) {
277+
Napi::Env env = info.Env();
278+
RclHandle* timer_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
279+
rcl_timer_t* timer = reinterpret_cast<rcl_timer_t*>(timer_handle->ptr());
280+
281+
if (!info[1].IsFunction()) {
282+
Napi::TypeError::New(env, "Callback must be a function")
283+
.ThrowAsJavaScriptException();
284+
return env.Undefined();
285+
}
286+
287+
Napi::Function callback = info[1].As<Napi::Function>();
288+
289+
std::lock_guard<std::mutex> lock(g_timer_contexts_mutex);
290+
std::shared_ptr<TimerContext> context;
291+
auto it = g_timer_contexts.find(timer);
292+
if (it == g_timer_contexts.end()) {
293+
context = std::make_shared<TimerContext>();
294+
g_timer_contexts[timer] = context;
295+
} else {
296+
context = it->second;
297+
context->on_reset_callback.Release();
298+
}
299+
300+
context->on_reset_callback = Napi::ThreadSafeFunction::New(
301+
env, callback, "TimerOnResetCallback", 0, 1);
302+
303+
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK,
304+
rcl_timer_set_on_reset_callback(
305+
timer, TimerOnResetCallbackTrampoline, timer),
306+
rcl_get_error_string().str);
307+
308+
return env.Undefined();
309+
}
310+
311+
Napi::Value ClearTimerOnResetCallback(const Napi::CallbackInfo& info) {
312+
Napi::Env env = info.Env();
313+
RclHandle* timer_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
314+
rcl_timer_t* timer = reinterpret_cast<rcl_timer_t*>(timer_handle->ptr());
315+
316+
std::lock_guard<std::mutex> lock(g_timer_contexts_mutex);
317+
auto it = g_timer_contexts.find(timer);
318+
if (it != g_timer_contexts.end()) {
319+
it->second->on_reset_callback.Release();
320+
g_timer_contexts.erase(it);
321+
}
322+
323+
THROW_ERROR_IF_NOT_EQUAL(
324+
RCL_RET_OK, rcl_timer_set_on_reset_callback(timer, nullptr, nullptr),
325+
rcl_get_error_string().str);
326+
327+
return env.Undefined();
328+
}
218329
#endif
219330

220331
Napi::Object InitTimerBindings(Napi::Env env, Napi::Object exports) {
@@ -231,6 +342,10 @@ Napi::Object InitTimerBindings(Napi::Env env, Napi::Object exports) {
231342
exports.Set("changeTimerPeriod", Napi::Function::New(env, ChangeTimerPeriod));
232343
exports.Set("getTimerPeriod", Napi::Function::New(env, GetTimerPeriod));
233344
#if ROS_VERSION > 2205 // 2205 == Humble
345+
exports.Set("setTimerOnResetCallback",
346+
Napi::Function::New(env, SetTimerOnResetCallback));
347+
exports.Set("clearTimerOnResetCallback",
348+
Napi::Function::New(env, ClearTimerOnResetCallback));
234349
exports.Set("callTimerWithInfo", Napi::Function::New(env, CallTimerWithInfo));
235350
#endif
236351
return exports;

test/test-timer.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,64 @@ describe('rclnodejs Timer class testing', function () {
166166
timer.cancel();
167167
done();
168168
});
169+
170+
it('timer.setOnResetCallback', function (done) {
171+
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
172+
this.skip();
173+
return;
174+
}
175+
var timer = node.createTimer(TIMER_INTERVAL, function () {});
176+
var called = false;
177+
timer.setOnResetCallback(function (events) {
178+
assert.strictEqual(typeof events, 'number');
179+
assert.ok(events >= 0);
180+
called = true;
181+
});
182+
timer.reset();
183+
184+
setTimeout(() => {
185+
assert.ok(called);
186+
timer.cancel();
187+
done();
188+
}, 100);
189+
190+
rclnodejs.spin(node);
191+
});
192+
193+
it('timer.clearOnResetCallback', function (done) {
194+
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
195+
this.skip();
196+
return;
197+
}
198+
var timer = node.createTimer(TIMER_INTERVAL, function () {});
199+
var called = false;
200+
timer.setOnResetCallback(function (events) {
201+
assert.strictEqual(typeof events, 'number');
202+
assert.ok(events >= 0);
203+
called = true;
204+
});
205+
timer.clearOnResetCallback();
206+
timer.reset();
207+
208+
setTimeout(() => {
209+
assert.ok(!called);
210+
timer.cancel();
211+
done();
212+
}, 100);
213+
214+
rclnodejs.spin(node);
215+
});
216+
217+
it('timer callback should be called repeatedly', function (done) {
218+
let count = 0;
219+
const timer = node.createTimer(TIMER_INTERVAL, () => {
220+
count++;
221+
if (count >= 3) {
222+
timer.cancel();
223+
done();
224+
}
225+
});
226+
rclnodejs.spin(node);
227+
});
169228
});
170229
});

test/types/index.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ expectType<boolean>(timer.isCanceled());
289289
expectType<void>(timer.cancel());
290290
expectType<void>(timer.changeTimerPeriod(BigInt(100000)));
291291
expectType<bigint>(timer.timerPeriod());
292+
expectType<void>(timer.setOnResetCallback((_events: number) => {}));
293+
expectType<void>(timer.clearOnResetCallback());
292294
expectType<object>(timer.callTimerWithInfo());
293295

294296
// ---- Rate ----

types/timer.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ declare module 'rclnodejs' {
5858
*/
5959
timerPeriod(): bigint;
6060

61+
/**
62+
* Set the on reset callback.
63+
* @param callback - The callback to be called when the timer is reset.
64+
*/
65+
setOnResetCallback(callback: (events: number) => void): void;
66+
67+
/**
68+
* Clear the on reset callback.
69+
*/
70+
clearOnResetCallback(): void;
71+
6172
/**
6273
* Call a timer and starts counting again, retrieves actual and expected call time.
6374
* @return - The timer information.

0 commit comments

Comments
 (0)