Skip to content

Commit d58aa40

Browse files
authored
Add Rate for #564 (#593)
Implements a Rate timer based on the rclpy Rate timer. See issue #564. Includes: - update node, added node#createRate(hz) - lib/rate.js - types/rate.d.ts - test/test-rate.js - updated test/types/main.ts Note for reviewers: - See RateTimer in rate.js. This class runs a Timer in private rcl context. Fix #564
1 parent 4290ef4 commit d58aa40

File tree

8 files changed

+477
-0
lines changed

8 files changed

+477
-0
lines changed

example/rate-example.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License");
2+
// you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
13+
'use strict';
14+
15+
const rclnodejs = require('../index.js');
16+
17+
/**
18+
* This example demonstrates a rate limited loop running at
19+
* 0.5 hz (once every 2 secs) and a publisher sending messages
20+
* every 10 ms from an setInterval(). Thus, the subscriber is
21+
* receiving only every 200th published message.
22+
*
23+
* To see every published message run this from commandline:
24+
* > ros2 topic echo topic 'std_msgs/msg/String'
25+
*
26+
* @return {undefined}
27+
*/
28+
async function main() {
29+
await rclnodejs.init();
30+
const node = rclnodejs.createNode('test_node');
31+
const publisher = node.createPublisher('std_msgs/msg/String', 'topic');
32+
const subscriptions = node.createSubscription(
33+
'std_msgs/msg/String',
34+
'topic',
35+
undefined,
36+
msg => console.log(`Received(${Date.now()}): ${msg.data}`)
37+
);
38+
const rate = node.createRate(0.5);
39+
40+
setInterval(() => publisher.publish(`hello ${Date.now()}`), 10);
41+
42+
let forever = true;
43+
while (forever) {
44+
await rate.sleep();
45+
rclnodejs.spinOnce(node, 1000);
46+
}
47+
}
48+
49+
main();

lib/node.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const {
3030
const ParameterService = require('./parameter_service.js');
3131
const Publisher = require('./publisher.js');
3232
const QoS = require('./qos.js');
33+
const Rates = require('./rate.js');
3334
const Service = require('./service.js');
3435
const Subscription = require('./subscription.js');
3536
const TimeSource = require('./time_source.js');
@@ -55,6 +56,7 @@ class Node {
5556
this._services = [];
5657
this._timers = [];
5758
this._guards = [];
59+
this._rateTimerServer = null;
5860
this._parameterDescriptors = new Map();
5961
this._parameters = new Map();
6062
this._parameterService = null;
@@ -266,6 +268,36 @@ class Node {
266268
return timer;
267269
}
268270

271+
/**
272+
* Create a Rate.
273+
*
274+
* @param {number} hz - The frequency of the rate timer; default is 1 hz.
275+
* @returns {Rate} - New instance
276+
*/
277+
createRate(hz = 1) {
278+
if (typeof hz !== 'number') {
279+
throw new TypeError('Invalid argument');
280+
}
281+
282+
const MAX_RATE_HZ_IN_MILLISECOND = 1000.0;
283+
if (hz <= 0.0 || hz > MAX_RATE_HZ_IN_MILLISECOND) {
284+
throw new RangeError(
285+
`Hz must be between 0.0 and ${MAX_RATE_HZ_IN_MILLISECOND}`
286+
);
287+
}
288+
289+
// lazy initialize rateTimerServer
290+
if (!this._rateTimerServer) {
291+
this._rateTimerServer = new Rates.RateTimerServer(this);
292+
}
293+
294+
const period = Math.round(1000 / hz);
295+
const timer = this._rateTimerServer.createTimer(period);
296+
const rate = new Rates.Rate(hz, timer);
297+
298+
return rate;
299+
}
300+
269301
/**
270302
* Create a Publisher.
271303
* @param {function|string|object} typeClass - The ROS message class,
@@ -485,6 +517,11 @@ class Node {
485517
this._clients = [];
486518
this._services = [];
487519
this._guards = [];
520+
521+
if (this._rateTimerServer) {
522+
this._rateTimerServer.shutdown();
523+
this._rateTimerServer = null;
524+
}
488525
}
489526

490527
/**

lib/rate.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
'use strict';
15+
16+
const rclnodejs = require('bindings')('rclnodejs');
17+
const Context = require('./context.js');
18+
const NodeOptions = require('./node_options.js');
19+
20+
const NOP_FN = () => {};
21+
22+
/**
23+
* A timer that runs at a regular frequency (hz).
24+
*
25+
* A client calls Rate#sleep() to block until the end of the current cycle.
26+
* This makes Rate useful for looping at a regular frequency (hz). Rate#sleep()
27+
* avoids blocking the JS event-loop by returning a Promise that the caller
28+
* should block on, e.g. use 'await rate.sleep()'.
29+
*
30+
* Note that Rate.sleep() does not prevent rclnodejs from invoking callbacks
31+
* such as a subscription or client if the entity's node is spinning. Thus
32+
* if your intent is to use rate to synchronize when callbacks are invoked
33+
* then use a spinOnce() just after rate.sleep() as in the example below.
34+
*
35+
* Rate runs within it's own private rcl context. This enables it to be
36+
* available immediately after construction. That is, unlike Timer, Rate
37+
* does not require a spin or spinOnce to be active.
38+
*
39+
* @example
40+
* async function run() {
41+
* await rclnodejs.init();
42+
* const node = rclnodejs.createNode('mynode');
43+
* const rate = node.createRate(1); // 1 hz
44+
* while (true) {
45+
* doSomeStuff();
46+
* await rate.sleep();
47+
* rclnodejs.spinOnce(node);
48+
* }
49+
* }
50+
*/
51+
class Rate {
52+
/**
53+
* Create a new instance.
54+
* @hideconstructor
55+
* @param {number} hz - The frequency (hz) between (0.0,1000] hz,
56+
* @param {Timer} timer - The internal timer used by this instance.
57+
* default = 1 hz
58+
*/
59+
constructor(hz, timer) {
60+
this._hz = hz;
61+
this._timer = timer;
62+
}
63+
64+
/**
65+
* Get the frequency in hertz (hz) of this timer.
66+
*
67+
* @returns {number} - hertz
68+
*/
69+
get frequency() {
70+
return this._hz;
71+
}
72+
73+
/**
74+
* Returns a Promise that when waited on, will block the sender
75+
* until the end of the current timer cycle.
76+
*
77+
* If the Rate has been cancelled, calling this method will
78+
* result in an error.
79+
*
80+
* @example
81+
* (async () => {
82+
* await rate.sleep();
83+
* })();
84+
*
85+
* @returns {Promise} - Waiting on the promise will delay the sender
86+
* (not the Node event-loop) until the end of the current timer cycle.
87+
*/
88+
async sleep() {
89+
if (this.isCanceled()) {
90+
throw new Error('Rate has been cancelled.');
91+
}
92+
93+
return new Promise(resolve => {
94+
this._timer.callback = () => {
95+
this._timer.callback = NOP_FN;
96+
resolve();
97+
};
98+
});
99+
}
100+
101+
/**
102+
* Permanently stops the timing behavior.
103+
*
104+
* @returns {undefined}
105+
*/
106+
cancel() {
107+
this._timer.cancel();
108+
}
109+
110+
/**
111+
* Determine if this rate has been cancelled.
112+
*
113+
* @returns {boolean} - True when cancel() has been called; False otherwise.
114+
*/
115+
isCanceled() {
116+
return this._timer.isCanceled();
117+
}
118+
}
119+
120+
/**
121+
* Internal class that creates Timer instances in a common private rcl context
122+
* for use with Rate. The private rcl context ensures that Rate timers do not
123+
* deadlock waiting for spinOnce/spin on the main rcl context.
124+
*/
125+
class RateTimerServer {
126+
/**
127+
* Create a new instance.
128+
*
129+
* @constructor
130+
* @param {Node} parentNode - The parent node for which this server
131+
* supplies timers to.
132+
*/
133+
constructor(parentNode) {
134+
this._context = new Context();
135+
136+
// init rcl environment
137+
rclnodejs.init(this._context.handle());
138+
139+
// create hidden node
140+
const nodeName = `_${parentNode.name()}_rate_timer_server`;
141+
const nodeNamespace = parentNode.namespace();
142+
this._node = new rclnodejs.ShadowNode();
143+
this._node.handle = rclnodejs.createNode(
144+
nodeName,
145+
nodeNamespace,
146+
this._context.handle()
147+
);
148+
149+
const options = new NodeOptions();
150+
options.startParameterServices = false;
151+
options.parameterOverrides = parentNode.getParameters();
152+
options.automaticallyDeclareParametersFromOverrides = true;
153+
154+
this._node.init(nodeName, nodeNamespace, this._context, options);
155+
156+
// spin node
157+
this._node.startSpinning(this._context.handle(), 10);
158+
}
159+
160+
/**
161+
* Create a new timer instance with callback set to NOP.
162+
*
163+
* @param {number} period - The period in milliseconds
164+
* @returns {Timer} - The new timer instance.
165+
*/
166+
createTimer(period) {
167+
const timer = this._node.createTimer(period, () => {}, this._context);
168+
return timer;
169+
}
170+
171+
/**
172+
* Permanently cancel all timers produced by this server and discontinue
173+
* the ability to create new Timers.
174+
*
175+
* The private rcl context is shutdown in the process and may not be
176+
* restarted.
177+
*
178+
* @returns {undefined}
179+
*/
180+
shutdown() {
181+
this._node.destroy();
182+
this._context.shutdown();
183+
}
184+
}
185+
// module.exports = {Rate, RateTimerServer};
186+
module.exports = { Rate, RateTimerServer };

0 commit comments

Comments
 (0)