Skip to content

Commit bc18c54

Browse files
authored
Add ClockEvent support (#1354)
This PR adds comprehensive ClockEvent support to rclnodejs, enabling clock-based sleep functionality for STEADY_TIME, SYSTEM_TIME, and ROS_TIME clocks. ### New Features **ClockEvent Class** - Thread-safe event synchronization for clock-based waiting - Support for steady, system, and ROS clock types - Async worker pattern for non-blocking sleep operations - Clock epoch synchronization between RCL and std::chrono **Clock Sleep Methods** - `Clock.sleepUntil(until, context)` - Sleep until absolute time point - `Clock.sleepFor(duration, context)` - Sleep for specified duration - Clock jump callbacks to wake on time changes - Context-aware early wakeup on shutdown - Detects ROS time activation/deactivation **ClockChange Enum** - `ROS_TIME_NO_CHANGE`, `ROS_TIME_ACTIVATED`, `ROS_TIME_DEACTIVATED`, `SYSTEM_TIME_NO_CHANGE` - Used in clock jump callback notifications ### Critical Fixes **BigInt Precision Loss Prevention** - Added lossless conversion checks for nanosecond timestamps - Prevents silent data corruption when converting BigInt to int64_t **Missing Module Imports** - Fixed missing Context, ClockEvent, and ClockChange imports in clock.js ### Test Coverage - **test-clock-event.js** - Basic ClockEvent operations (4 tests) - **test-clock-sleep.js** - Sleep methods for all clock types (11 tests) - Includes comprehensive ROS time active scenario with TimeSource + simulated clock messages - **test-clock-change.js** - ClockChange enum and integration tests (11 tests) **Results**: 1055 passing, 6 pending ### Files Changed **Added**: - `src/clock_event.{cpp,hpp}`, clock_event.js, clock_change.js - `types/clock_event.d.ts`, `types/clock_change.d.ts` - `test/test-clock-{event,sleep,change}.js` **Modified**: - clock.js - Added sleep methods and imports - `types/clock.d.ts`, index.d.ts - Added type definitions - `binding.gyp`, `index.js`, `src/addon.cpp` - Registered bindings **Impact**: +1397 lines, fully backward compatible, no breaking changes Fix: #1330
1 parent afeeae4 commit bc18c54

17 files changed

+1401
-1
lines changed

binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'./src/rcl_action_server_bindings.cpp',
2727
'./src/rcl_bindings.cpp',
2828
'./src/rcl_client_bindings.cpp',
29+
'./src/clock_event.cpp',
2930
'./src/rcl_context_bindings.cpp',
3031
'./src/rcl_graph_bindings.cpp',
3132
'./src/rcl_guard_condition_bindings.cpp',

index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
const DistroUtils = require('./lib/distro.js');
1818
const RMWUtils = require('./lib/rmw.js');
1919
const { Clock, ROSClock } = require('./lib/clock.js');
20+
const ClockEvent = require('./lib/clock_event.js');
2021
const ClockType = require('./lib/clock_type.js');
22+
const ClockChange = require('./lib/clock_change.js');
2123
const { compareVersions } = require('./lib/utils.js');
2224
const Context = require('./lib/context.js');
2325
const debug = require('debug')('rclnodejs');
@@ -189,9 +191,15 @@ let rcl = {
189191
/** {@link Clock} class */
190192
Clock: Clock,
191193

194+
/** {@link ClockEvent} class */
195+
ClockEvent: ClockEvent,
196+
192197
/** {@link ClockType} enum */
193198
ClockType: ClockType,
194199

200+
/** {@link ClockChange} enum */
201+
ClockChange: ClockChange,
202+
195203
/** {@link Context} class */
196204
Context: Context,
197205

lib/clock.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
const rclnodejs = require('./native_loader.js');
1818
const Time = require('./time.js');
1919
const ClockType = require('./clock_type.js');
20+
const ClockChange = require('./clock_change.js');
21+
const Context = require('./context.js');
22+
const ClockEvent = require('./clock_event.js');
2023
const { TypeValidationError } = require('./errors.js');
2124

2225
/**
@@ -121,6 +124,116 @@ class Clock {
121124
const nowInNanosec = rclnodejs.clockGetNow(this._handle);
122125
return new Time(0n, nowInNanosec, this._clockType);
123126
}
127+
128+
/**
129+
* Sleep until a specific time is reached on this Clock.
130+
*
131+
* When using a ROSClock, this may sleep forever if the TimeSource is misconfigured
132+
* and the context is never shut down. ROS time being activated or deactivated causes
133+
* this function to cease sleeping and return false.
134+
*
135+
* @param {Time} until - Time at which this function should stop sleeping.
136+
* @param {Context} [context=null] - Context whose validity will be checked while sleeping.
137+
* If context is null, then the default context is used. If the context is found to be
138+
* shut down before or by the time the wait completes, the returned promise resolves to false.
139+
* @return {Promise<boolean>} Promise that resolves to true if 'until' was reached without
140+
* detecting a time jump or a shut down context, or false otherwise (for example, if a time
141+
* jump occurred or the context is no longer valid when the wait completes).
142+
* @throws {TypeError} if until is specified for a different type of clock than this one.
143+
* @throws {Error} if context has not been initialized or is shutdown.
144+
*/
145+
async sleepUntil(until, context = null) {
146+
if (!(until instanceof Time)) {
147+
throw new TypeValidationError('until', until, 'Time', {
148+
entityType: 'clock',
149+
});
150+
}
151+
152+
if (!context) {
153+
context = Context.defaultContext();
154+
}
155+
156+
if (!context.isValid()) {
157+
throw new Error('Context is not initialized or has been shut down');
158+
}
159+
160+
if (until.clockType !== this._clockType) {
161+
throw new TypeError(
162+
"until's clock type does not match this clock's type"
163+
);
164+
}
165+
166+
const event = new ClockEvent();
167+
let timeSourceChanged = false;
168+
169+
// Callback to wake when time jumps and is past target time
170+
const callbackObject = {
171+
_pre_callback: () => {
172+
// Optional pre-callback - no action needed
173+
},
174+
_post_callback: (jumpInfo) => {
175+
// ROS time being activated or deactivated changes the epoch,
176+
// so sleep time loses its meaning
177+
timeSourceChanged =
178+
timeSourceChanged ||
179+
jumpInfo.clock_change === ClockChange.ROS_TIME_ACTIVATED ||
180+
jumpInfo.clock_change === ClockChange.ROS_TIME_DEACTIVATED;
181+
182+
if (timeSourceChanged || this.now().nanoseconds >= until.nanoseconds) {
183+
event.set();
184+
}
185+
},
186+
};
187+
188+
// Setup jump callback with minimal forward threshold
189+
this.addClockCallback(
190+
callbackObject,
191+
true, // onClockChange
192+
1n, // minForward (1 nanosecond - any forward jump triggers)
193+
0n // minBackward (0 disables backward threshold)
194+
);
195+
196+
try {
197+
// Wait based on clock type
198+
if (this._clockType === ClockType.SYSTEM_TIME) {
199+
await event.waitUntilSystem(this, until.nanoseconds);
200+
} else if (this._clockType === ClockType.STEADY_TIME) {
201+
await event.waitUntilSteady(this, until.nanoseconds);
202+
} else if (this._clockType === ClockType.ROS_TIME) {
203+
await event.waitUntilRos(this, until.nanoseconds);
204+
}
205+
} finally {
206+
// Always clean up callback
207+
this.removeClockCallback(callbackObject);
208+
}
209+
210+
if (!context.isValid() || timeSourceChanged) {
211+
return false;
212+
}
213+
214+
return this.now().nanoseconds >= until.nanoseconds;
215+
}
216+
217+
/**
218+
* Sleep for a specified duration.
219+
*
220+
* Equivalent to: clock.sleepUntil(clock.now() + duration, context)
221+
*
222+
* When using a ROSClock, this may sleep forever if the TimeSource is misconfigured
223+
* and the context is never shut down. ROS time being activated or deactivated causes
224+
* this function to cease sleeping and return false.
225+
*
226+
* @param {Duration} duration - Duration of time to sleep for.
227+
* @param {Context} [context=null] - Context which when shut down will cause this sleep to wake early.
228+
* If context is null, then the default context is used.
229+
* @return {Promise<boolean>} Promise that resolves to true if the full duration was slept,
230+
* or false if it woke for another reason.
231+
* @throws {Error} if context has not been initialized or is shutdown.
232+
*/
233+
async sleepFor(duration, context = null) {
234+
const until = this.now().add(duration);
235+
return this.sleepUntil(until, context);
236+
}
124237
}
125238

126239
/**

lib/clock_change.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) 2025 The Robot Web Tools Contributors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
/**
18+
* Enum for ClockChange
19+
* Represents the type of clock change that occurred during a time jump.
20+
* @readonly
21+
* @enum {number}
22+
*/
23+
const ClockChange = {
24+
/**
25+
* The source before and after the jump is ROS_TIME.
26+
* @member {number}
27+
*/
28+
ROS_TIME_NO_CHANGE: 1,
29+
30+
/**
31+
* The source switched to ROS_TIME from SYSTEM_TIME.
32+
* @member {number}
33+
*/
34+
ROS_TIME_ACTIVATED: 2,
35+
36+
/**
37+
* The source switched to SYSTEM_TIME from ROS_TIME.
38+
* @member {number}
39+
*/
40+
ROS_TIME_DEACTIVATED: 3,
41+
42+
/**
43+
* The source before and after the jump is SYSTEM_TIME.
44+
* @member {number}
45+
*/
46+
SYSTEM_TIME_NO_CHANGE: 4,
47+
};
48+
49+
module.exports = ClockChange;

lib/clock_event.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) 2025, The Robot Web Tools Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const rclnodejs = require('./native_loader.js');
18+
19+
/**
20+
* @class - Class representing a ClockEvent in ROS
21+
*/
22+
class ClockEvent {
23+
constructor() {
24+
this._handle = rclnodejs.createClockEvent();
25+
}
26+
27+
/**
28+
* Wait until a time specified by a steady clock.
29+
* @param {Clock} clock - The clock to use for time synchronization.
30+
* @param {bigint} until - The time to wait until.
31+
* @return {Promise<void>} - A promise that resolves when the time is reached.
32+
*/
33+
async waitUntilSteady(clock, until) {
34+
return rclnodejs.clockEventWaitUntilSteady(
35+
this._handle,
36+
clock.handle,
37+
until
38+
);
39+
}
40+
41+
/**
42+
* Wait until a time specified by a system clock.
43+
* @param {Clock} clock - The clock to use for time synchronization.
44+
* @param {bigint} until - The time to wait until.
45+
* @return {Promise<void>} - A promise that resolves when the time is reached.
46+
*/
47+
async waitUntilSystem(clock, until) {
48+
return rclnodejs.clockEventWaitUntilSystem(
49+
this._handle,
50+
clock.handle,
51+
until
52+
);
53+
}
54+
55+
/**
56+
* Wait until a time specified by a ROS clock.
57+
* @param {Clock} clock - The clock to use for time synchronization.
58+
* @param {bigint} until - The time to wait until.
59+
* @return {Promise<void>} - A promise that resolves when the time is reached.
60+
*/
61+
async waitUntilRos(clock, until) {
62+
return rclnodejs.clockEventWaitUntilRos(this._handle, clock.handle, until);
63+
}
64+
65+
/**
66+
* Indicate if the ClockEvent is set.
67+
* @return {boolean} - True if the ClockEvent is set.
68+
*/
69+
isSet() {
70+
return rclnodejs.clockEventIsSet(this._handle);
71+
}
72+
73+
/**
74+
* Set the event.
75+
*/
76+
set() {
77+
rclnodejs.clockEventSet(this._handle);
78+
}
79+
80+
/**
81+
* Clear the event.
82+
*/
83+
clear() {
84+
rclnodejs.clockEventClear(this._handle);
85+
}
86+
}
87+
88+
module.exports = ClockEvent;

src/addon.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <node_api.h>
1616
#include <rcutils/logging.h>
1717

18+
#include "clock_event.hpp"
1819
#include "macros.h"
1920
#include "rcl_action_client_bindings.h"
2021
#include "rcl_action_goal_bindings.h"
@@ -74,6 +75,7 @@ Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
7475
rclnodejs::InitActionGoalBindings(env, exports);
7576
rclnodejs::InitActionServerBindings(env, exports);
7677
rclnodejs::InitClientBindings(env, exports);
78+
rclnodejs::InitClockEventBindings(env, exports);
7779
rclnodejs::InitContextBindings(env, exports);
7880
rclnodejs::InitGraphBindings(env, exports);
7981
rclnodejs::InitGuardConditionBindings(env, exports);

0 commit comments

Comments
 (0)