Skip to content

Commit 6c63a81

Browse files
committedMar 3, 2023
ROS Humble introduced the content-filtering topics feature. This PR
makes makes this feature available to rclnodejs developers. node.js - added contentFilter to Options - added static getDefaultOptions() - updated createSubscription() to support contentFilter node.d.ts - added content-filter types subscription.js - isContentFilteringEnabled() - setContentFilter() - clearContentFilter() subscription.d.ts - updated with content-filter api rcl_bindings.cpp - added content-filtering to CreateSubscription() rmw.js - new class for identifying the current ROS middleware test-subscription-content-filter.js - test cases for content-filters test/blocklist.json - added test-subscription-content-filter.js for Windows and Mac OS examples: - publisher-content-filtering-example.js - subscription-content-filtering-example.js package.json - added build/rebuild scripts for convenience
1 parent feb8e03 commit 6c63a81

17 files changed

+1064
-23
lines changed
 

‎.github/workflows/windows-build-and-test-compatibility.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
fail-fast: false
1212
matrix:
13-
node-version: [14.21.2, 16.19.0, 18.14.2, 19.X]
13+
node-version: [14.21.2, 16.19.0, 18.14.1, 19.X]
1414
ros_distribution:
1515
- foxy
1616
- humble

‎docs/EFFICIENCY.md

+44-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Tips for efficent use of rclnodejs
2-
While our benchmarks place rclnodejs performance at or above that of [rclpy](https://github.com/ros2/rclpy) we recommend appyling efficient coding and configuration practices where applicable.
2+
3+
While our benchmarks place rclnodejs performance at or above that of [rclpy](https://github.com/ros2/rclpy) we recommend appyling efficient coding and configuration practices where applicable.
34

45
## Tip-1: Disable Parameter Services
6+
57
The typical ROS 2 node creation process includes creating an internal parameter service who's job is to fulfill requests for parameter meta-data and to set and update node parameters. If your ROS 2 node does not support public parameters then you can save the resources consumed by the parameter service. Disable the node parameter service by setting the `NodeOption.startParameterServices` property to false as shown below:
68

79
```
@@ -13,16 +15,54 @@ let node = new Node(nodeName, namespace, Context.defaultContext(), options);
1315
```
1416

1517
## Tip-2: Disable LifecycleNode Lifecycle Services
18+
1619
The LifecycleNode constructor creates 5 life-cycle services to support the ROS 2 lifecycle specification. If your LifecycleNode instance will not be operating in a managed-node context consider disabling the lifecycle services via the LifecycleNode constructor as shown:
1720

1821
```
1922
let enableLifecycleCommInterface = false;
2023
2124
let node = new LifecycleNode(
22-
nodeName,
25+
nodeName,
2326
namespace,
24-
Context.defaultContext,
27+
Context.defaultContext,
2528
NodeOptions.defaultOptions,
26-
enableLifecycleCommInterface
29+
enableLifecycleCommInterface
2730
);
2831
```
32+
33+
## Tip-3: Use Content-filtering Subscriptions
34+
35+
The ROS Humble release introduced content-filtering topics
36+
which enable a subscription to limit the messages it receives
37+
to a subset of interest. While the application of the a content-filter
38+
is specific to the DDS/RMW vendor, the general approach is to apply
39+
filtering on the publisher side. This can reduce network bandwidth
40+
for pub-sub communications and message processing and memory
41+
overhead of rclnodejs nodes.
42+
43+
Note: Be sure to confirm that your RMW implementation supports
44+
content-filter before attempting to use it. In cases where content-filtering
45+
is not supported your Subscription will simply ignore your filter and
46+
continue operating with no filtering.
47+
48+
Example:
49+
50+
```
51+
// create a content-filter to limit incoming messages to
52+
// only those with temperature > 75C.
53+
const options = rclnodejs.Node.getDefaultOptions();
54+
options.contentFilter = {
55+
expression: 'temperature > %0',
56+
parameters: [75],
57+
};
58+
59+
node.createSubscription(
60+
'sensor_msgs/msg/Temperature',
61+
'temperature',
62+
options,
63+
(temperatureMsg) => {
64+
console.log(`EMERGENCY temperature detected: ${temperatureMsg.temperature}`);
65+
}
66+
);
67+
68+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2017 Intel Corporation. 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+
/* eslint-disable camelcase */
18+
19+
const rclnodejs = require('../index.js');
20+
21+
async function main() {
22+
await rclnodejs.init();
23+
const node = new rclnodejs.Node('publisher_content_filter_example_node');
24+
const publisher = node.createPublisher(
25+
'sensor_msgs/msg/Temperature',
26+
'temperature'
27+
);
28+
29+
let count = 0;
30+
setInterval(function () {
31+
let temperature = (Math.random() * 100).toFixed(2);
32+
33+
publisher.publish({
34+
header: {
35+
stamp: {
36+
sec: 123456,
37+
nanosec: 789,
38+
},
39+
frame_id: 'main frame',
40+
},
41+
temperature: temperature,
42+
variance: 0,
43+
});
44+
45+
console.log(
46+
`Publish temerature message-${++count}: ${temperature} degrees`
47+
);
48+
}, 750);
49+
50+
node.spin();
51+
}
52+
53+
main();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) 2023 Wayne Parrott. 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+
const { assertDefined } = require('dtslint/bin/util.js');
18+
const rclnodejs = require('../index.js');
19+
20+
/**
21+
* This example demonstrates the use of content-filtering
22+
* topics (subscriptions) that were introduced in ROS 2 Humble.
23+
* See the following resources for content-filtering in ROS:
24+
* @see {@link Node#options}
25+
* @see {@link Node#createSubscription}
26+
* @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
27+
*
28+
* Use publisher-content-filter-example.js to generate example messages.
29+
*
30+
* To see all published messages (filterd + unfiltered) run this
31+
* from commandline:
32+
*
33+
* ros2 topic echo temperature
34+
*
35+
* @return {undefined}
36+
*/
37+
async function main() {
38+
await rclnodejs.init();
39+
const node = new rclnodejs.Node('subscription_message_example_node');
40+
41+
let param = 50;
42+
43+
// create a content-filter to limit incoming messages to
44+
// only those with temperature > paramC.
45+
const options = rclnodejs.Node.getDefaultOptions();
46+
options.contentFilter = {
47+
expression: 'temperature > %0',
48+
parameters: [param],
49+
};
50+
51+
let count = 0;
52+
let subscription;
53+
try {
54+
subscription = node.createSubscription(
55+
'sensor_msgs/msg/Temperature',
56+
'temperature',
57+
options,
58+
(temperatureMsg) => {
59+
console.log(`Received temperature message-${++count}:
60+
${temperatureMsg.temperature}C`);
61+
if (count % 5 === 0) {
62+
if (subscription.isContentFilteringEnabled()) {
63+
console.log('Clearing filter');
64+
subscription.clearContentFilter();
65+
} else {
66+
param += 10;
67+
console.log('Update topic content-filter, temperature > ', param);
68+
const contentFilter = {
69+
expression: 'temperature > %0',
70+
parameters: [param],
71+
};
72+
subscription.setContentFilter(contentFilter);
73+
}
74+
console.log(
75+
'Content-filtering enabled: ',
76+
subscription.isContentFilteringEnabled()
77+
);
78+
}
79+
}
80+
);
81+
82+
if (!subscription.isContentFilteringEnabled()) {
83+
console.log('Content-filtering is not enabled on subscription.');
84+
}
85+
} catch (error) {
86+
console.error('Unable to create content-filtering subscription.');
87+
console.error(
88+
'Please ensure your content-filter expression and parameters are well-formed.'
89+
);
90+
}
91+
92+
node.spin();
93+
}
94+
95+
main();

‎index.js

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'use strict';
1616

1717
const DistroUtils = require('./lib/distro.js');
18+
const RMWUtils = require('./lib/rmw.js');
1819
const { Clock, ROSClock } = require('./lib/clock.js');
1920
const ClockType = require('./lib/clock_type.js');
2021
const compareVersions = require('compare-versions');
@@ -136,6 +137,9 @@ let rcl = {
136137
/** {@link QoS} class */
137138
QoS: QoS,
138139

140+
/** {@link RMWUtils} */
141+
RMWUtils: RMWUtils,
142+
139143
/** {@link ROSClock} class */
140144
ROSClock: ROSClock,
141145

‎lib/distro.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const DistroUtils = {
4242
* @return {number} Return the rclnodejs distro identifier
4343
*/
4444
getDistroId: function (distroName) {
45-
const dname = distroName ? distroName : this.getDistroName();
45+
const dname = distroName ? distroName.toLowerCase() : this.getDistroName();
4646

4747
return DistroNameIdMap.has(dname)
4848
? DistroNameIdMap.get(dname)

‎lib/node.js

+36-7
Original file line numberDiff line numberDiff line change
@@ -464,12 +464,7 @@ class Node extends rclnodejs.ShadowNode {
464464
}
465465

466466
if (options === undefined) {
467-
options = {
468-
enableTypedArray: true,
469-
isRaw: false,
470-
qos: QoS.profileDefault,
471-
};
472-
return options;
467+
return Node.getDefaultOptions();
473468
}
474469

475470
if (options.enableTypedArray === undefined) {
@@ -484,6 +479,10 @@ class Node extends rclnodejs.ShadowNode {
484479
options = Object.assign(options, { isRaw: false });
485480
}
486481

482+
if (options.contentFilter === undefined) {
483+
options = Object.assign(options, { contentFilter: undefined });
484+
}
485+
487486
return options;
488487
}
489488

@@ -608,7 +607,7 @@ class Node extends rclnodejs.ShadowNode {
608607
*/
609608

610609
/**
611-
* Create a Subscription.
610+
* Create a Subscription with optional content-filtering.
612611
* @param {function|string|object} typeClass - The ROS message class,
613612
OR a string representing the message class, e.g. 'std_msgs/msg/String',
614613
OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
@@ -617,9 +616,18 @@ class Node extends rclnodejs.ShadowNode {
617616
* @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
618617
* @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
619618
* @param {boolean} options.isRaw - The topic is serialized when true, default: false.
619+
* @param {object} [options.contentFilter=undefined] - The content-filter, default: undefined.
620+
* Confirm that your RMW supports content-filtered topics before use.
621+
* @param {string} options.contentFilter.expression - Specifies the criteria to select the data samples of
622+
* interest. It is similar to the WHERE part of an SQL clause.
623+
* @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
624+
* the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
625+
* fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
620626
* @param {SubscriptionCallback} callback - The callback to be call when receiving the topic subscribed. The topic will be an instance of null-terminated Buffer when options.isRaw is true.
621627
* @return {Subscription} - An instance of Subscription.
628+
* @throws {ERROR} - May throw an RMW error if content-filter is malformed.
622629
* @see {@link SubscriptionCallback}
630+
* @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B}
623631
*/
624632
createSubscription(typeClass, topic, options, callback) {
625633
if (typeof typeClass === 'string' || typeof typeClass === 'object') {
@@ -1645,4 +1653,25 @@ class Node extends rclnodejs.ShadowNode {
16451653
}
16461654
}
16471655

1656+
/**
1657+
* Create an Options instance initialized with default values.
1658+
* @returns {Options} - The new initialized instance.
1659+
* @static
1660+
* @example
1661+
* {
1662+
* enableTypedArray: true,
1663+
* isRaw: false,
1664+
* qos: QoS.profileDefault,
1665+
* contentFilter: undefined,
1666+
* }
1667+
*/
1668+
Node.getDefaultOptions = function () {
1669+
return {
1670+
enableTypedArray: true,
1671+
isRaw: false,
1672+
qos: QoS.profileDefault,
1673+
contentFilter: undefined,
1674+
};
1675+
};
1676+
16481677
module.exports = Node;

‎lib/rmw.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const DistroUtils = require('./distro');
4+
5+
const RMWNames = {
6+
FASTRTPS: 'rmw_fastrtps_cpp',
7+
CONNEXT: 'rmw_connext_cpp',
8+
CYCLONEDDS: 'rmw_cyclonedds_cpp',
9+
GURUMDDS: 'rmw_gurumdds_cpp',
10+
};
11+
12+
const DefaultRosRMWNameMap = new Map();
13+
DefaultRosRMWNameMap.set('eloquent', RMWNames.FASTRTPS);
14+
DefaultRosRMWNameMap.set('foxy', RMWNames.FASTRTPS);
15+
DefaultRosRMWNameMap.set('galactic', RMWNames.CYCLONEDDS);
16+
DefaultRosRMWNameMap.set('humble', RMWNames.FASTRTPS);
17+
DefaultRosRMWNameMap.set('rolling', RMWNames.FASTRTPS);
18+
19+
const RMWUtils = {
20+
RMWNames: RMWNames,
21+
22+
getRMWName: function () {
23+
return process.env.RMW_IMPLEMENTATION
24+
? process.env.RMW_IMPLEMENTATION
25+
: DefaultRosRMWNameMap.get(DistroUtils.getDistroName());
26+
},
27+
};
28+
29+
module.exports = RMWUtils;

0 commit comments

Comments
 (0)