Skip to content

Commit be8615a

Browse files
authored
Add Clock callback support (#1355)
This PR adds support for clock callbacks in rclnodejs, allowing applications to be notified when time jumps occur (e.g., when ROS time is overridden or adjusted). This is useful for time-sensitive applications that need to handle discontinuities in the time stream. - Adds `addClockCallback` and `removeClockCallback` methods to the Clock class - Implements C++ bindings for registering/removing jump callbacks with RCL - Includes comprehensive test coverage for callback functionality with various threshold scenarios Fix: #1330
1 parent e6e95c1 commit be8615a

5 files changed

Lines changed: 427 additions & 0 deletions

File tree

lib/clock.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,66 @@ class Clock {
4848
return this._clockType;
4949
}
5050

51+
/**
52+
* Add a clock callback.
53+
* @param {object} callbackObject - The object containing _pre_callback and _post_callback methods.
54+
* @param {boolean} onClockChange - Whether to call the callback on clock change.
55+
* @param {bigint} minForward - Minimum forward jump to trigger the callback.
56+
* @param {bigint} minBackward - Minimum backward jump to trigger the callback.
57+
*/
58+
addClockCallback(callbackObject, onClockChange, minForward, minBackward) {
59+
if (typeof callbackObject !== 'object' || callbackObject === null) {
60+
throw new TypeValidationError(
61+
'callbackObject',
62+
callbackObject,
63+
'object',
64+
{
65+
entityType: 'clock',
66+
}
67+
);
68+
}
69+
if (typeof onClockChange !== 'boolean') {
70+
throw new TypeValidationError('onClockChange', onClockChange, 'boolean', {
71+
entityType: 'clock',
72+
});
73+
}
74+
if (typeof minForward !== 'bigint') {
75+
throw new TypeValidationError('minForward', minForward, 'bigint', {
76+
entityType: 'clock',
77+
});
78+
}
79+
if (typeof minBackward !== 'bigint') {
80+
throw new TypeValidationError('minBackward', minBackward, 'bigint', {
81+
entityType: 'clock',
82+
});
83+
}
84+
rclnodejs.clockAddJumpCallback(
85+
this._handle,
86+
callbackObject,
87+
onClockChange,
88+
minForward,
89+
minBackward
90+
);
91+
}
92+
93+
/**
94+
* Remove a clock callback.
95+
* @param {object} callbackObject - The object containing _pre_callback and _post_callback methods.
96+
*/
97+
removeClockCallback(callbackObject) {
98+
if (typeof callbackObject !== 'object' || callbackObject === null) {
99+
throw new TypeValidationError(
100+
'callbackObject',
101+
callbackObject,
102+
'object',
103+
{
104+
entityType: 'clock',
105+
}
106+
);
107+
}
108+
rclnodejs.clockRemoveJumpCallback(this._handle, callbackObject);
109+
}
110+
51111
/**
52112
* Return the current time.
53113
* @return {Time} Return the current time.

src/rcl_time_point_bindings.cpp

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,144 @@ Napi::Value ClockGetNow(const Napi::CallbackInfo& info) {
175175
return Napi::BigInt::New(env, time_point.nanoseconds);
176176
}
177177

178+
struct JumpCallbackData {
179+
Napi::ThreadSafeFunction tsfn_pre;
180+
Napi::ThreadSafeFunction tsfn_post;
181+
};
182+
183+
struct JumpCallbackContext {
184+
rcl_time_jump_t time_jump;
185+
bool before_jump;
186+
};
187+
188+
void _rclnodejs_on_time_jump(const rcl_time_jump_t* time_jump, bool before_jump,
189+
void* user_data) {
190+
JumpCallbackData* data = static_cast<JumpCallbackData*>(user_data);
191+
192+
auto context = new JumpCallbackContext{*time_jump, before_jump};
193+
194+
if (before_jump) {
195+
auto callback = [](Napi::Env env, Napi::Function js_callback,
196+
JumpCallbackContext* context) {
197+
js_callback.Call({});
198+
delete context;
199+
};
200+
data->tsfn_pre.NonBlockingCall(context, callback);
201+
} else {
202+
auto callback = [](Napi::Env env, Napi::Function js_callback,
203+
JumpCallbackContext* context) {
204+
Napi::Object jump_info = Napi::Object::New(env);
205+
jump_info.Set("clock_change",
206+
static_cast<int32_t>(context->time_jump.clock_change));
207+
jump_info.Set("delta", Napi::BigInt::New(
208+
env, context->time_jump.delta.nanoseconds));
209+
js_callback.Call({jump_info});
210+
delete context;
211+
};
212+
data->tsfn_post.NonBlockingCall(context, callback);
213+
}
214+
}
215+
216+
Napi::Value ClockAddJumpCallback(const Napi::CallbackInfo& info) {
217+
Napi::Env env = info.Env();
218+
219+
RclHandle* clock_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
220+
rcl_clock_t* clock = reinterpret_cast<rcl_clock_t*>(clock_handle->ptr());
221+
222+
Napi::Object callback_obj = info[1].As<Napi::Object>();
223+
Napi::Function pre_callback =
224+
callback_obj.Get("_pre_callback").As<Napi::Function>();
225+
Napi::Function post_callback =
226+
callback_obj.Get("_post_callback").As<Napi::Function>();
227+
228+
bool on_clock_change = info[2].As<Napi::Boolean>();
229+
230+
bool lossless;
231+
int64_t min_forward = info[3].As<Napi::BigInt>().Int64Value(&lossless);
232+
if (!lossless) {
233+
Napi::TypeError::New(
234+
env, "min_forward BigInt value cannot be represented as int64_t")
235+
.ThrowAsJavaScriptException();
236+
return env.Undefined();
237+
}
238+
int64_t min_backward = info[4].As<Napi::BigInt>().Int64Value(&lossless);
239+
if (!lossless) {
240+
Napi::TypeError::New(
241+
env, "min_backward BigInt value cannot be represented as int64_t")
242+
.ThrowAsJavaScriptException();
243+
return env.Undefined();
244+
}
245+
246+
rcl_jump_threshold_t threshold;
247+
threshold.on_clock_change = on_clock_change;
248+
threshold.min_forward.nanoseconds = min_forward;
249+
threshold.min_backward.nanoseconds = min_backward;
250+
251+
JumpCallbackData* data = new JumpCallbackData();
252+
data->tsfn_pre = Napi::ThreadSafeFunction::New(
253+
env, pre_callback, "ClockJumpPreCallback", 10, 1, [](Napi::Env) {});
254+
data->tsfn_post =
255+
Napi::ThreadSafeFunction::New(env, post_callback, "ClockJumpPostCallback",
256+
10, 1, [data](Napi::Env) { delete data; });
257+
258+
Napi::Object handle_obj = Napi::Object::New(env);
259+
handle_obj.Set("_cpp_handle",
260+
Napi::External<JumpCallbackData>::New(env, data));
261+
262+
rcl_ret_t ret = rcl_clock_add_jump_callback(clock, threshold,
263+
_rclnodejs_on_time_jump, data);
264+
265+
if (ret != RCL_RET_OK) {
266+
data->tsfn_pre.Release();
267+
data->tsfn_post.Release();
268+
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, ret, rcl_get_error_string().str);
269+
}
270+
271+
callback_obj.Set("_cpp_handle", handle_obj.Get("_cpp_handle"));
272+
273+
return env.Undefined();
274+
}
275+
276+
Napi::Value ClockRemoveJumpCallback(const Napi::CallbackInfo& info) {
277+
Napi::Env env = info.Env();
278+
279+
RclHandle* clock_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
280+
rcl_clock_t* clock = reinterpret_cast<rcl_clock_t*>(clock_handle->ptr());
281+
282+
Napi::Object handle_obj = info[1].As<Napi::Object>();
283+
Napi::Value cpp_handle = handle_obj.Get("_cpp_handle");
284+
285+
if (cpp_handle.IsUndefined() || !cpp_handle.IsExternal()) {
286+
Napi::Error::New(env,
287+
"Callback object was not registered or already removed")
288+
.ThrowAsJavaScriptException();
289+
return env.Undefined();
290+
}
291+
292+
JumpCallbackData* data =
293+
cpp_handle.As<Napi::External<JumpCallbackData>>().Data();
294+
295+
rcl_ret_t ret =
296+
rcl_clock_remove_jump_callback(clock, _rclnodejs_on_time_jump, data);
297+
298+
if (ret == RCL_RET_OK) {
299+
data->tsfn_pre.Release();
300+
data->tsfn_post.Release();
301+
handle_obj.Set("_cpp_handle", env.Undefined());
302+
} else {
303+
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, ret, rcl_get_error_string().str);
304+
}
305+
306+
return env.Undefined();
307+
}
308+
178309
Napi::Object InitTimePointBindings(Napi::Env env, Napi::Object exports) {
179310
exports.Set("createClock", Napi::Function::New(env, CreateClock));
180311
exports.Set("clockGetNow", Napi::Function::New(env, ClockGetNow));
312+
exports.Set("clockAddJumpCallback",
313+
Napi::Function::New(env, ClockAddJumpCallback));
314+
exports.Set("clockRemoveJumpCallback",
315+
Napi::Function::New(env, ClockRemoveJumpCallback));
181316
exports.Set("createTimePoint", Napi::Function::New(env, CreateTimePoint));
182317
exports.Set("getNanoseconds", Napi::Function::New(env, GetNanoseconds));
183318
exports.Set("createDuration", Napi::Function::New(env, CreateDuration));

0 commit comments

Comments
 (0)