Skip to content

Commit a9237b7

Browse files
authored
Introducing content-filtering topics (#901)
* 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 * Delete obsolete ./test.js * implements recommended PR feedback
1 parent feb8e03 commit a9237b7

16 files changed

+1072
-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) 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+
/* 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.hasContentFilter()) {
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.hasContentFilter()
77+
);
78+
}
79+
}
80+
);
81+
82+
if (!subscription.hasContentFilter()) {
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

+32-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) {
@@ -608,7 +603,7 @@ class Node extends rclnodejs.ShadowNode {
608603
*/
609604

610605
/**
611-
* Create a Subscription.
606+
* Create a Subscription with optional content-filtering.
612607
* @param {function|string|object} typeClass - The ROS message class,
613608
OR a string representing the message class, e.g. 'std_msgs/msg/String',
614609
OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
@@ -617,9 +612,18 @@ class Node extends rclnodejs.ShadowNode {
617612
* @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
618613
* @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
619614
* @param {boolean} options.isRaw - The topic is serialized when true, default: false.
615+
* @param {object} [options.contentFilter=undefined] - The content-filter, default: undefined.
616+
* Confirm that your RMW supports content-filtered topics before use.
617+
* @param {string} options.contentFilter.expression - Specifies the criteria to select the data samples of
618+
* interest. It is similar to the WHERE part of an SQL clause.
619+
* @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
620+
* the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
621+
* fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
620622
* @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.
621623
* @return {Subscription} - An instance of Subscription.
624+
* @throws {ERROR} - May throw an RMW error if content-filter is malformed.
622625
* @see {@link SubscriptionCallback}
626+
* @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B}
623627
*/
624628
createSubscription(typeClass, topic, options, callback) {
625629
if (typeof typeClass === 'string' || typeof typeClass === 'object') {
@@ -1645,4 +1649,25 @@ class Node extends rclnodejs.ShadowNode {
16451649
}
16461650
}
16471651

1652+
/**
1653+
* Create an Options instance initialized with default values.
1654+
* @returns {Options} - The new initialized instance.
1655+
* @static
1656+
* @example
1657+
* {
1658+
* enableTypedArray: true,
1659+
* isRaw: false,
1660+
* qos: QoS.profileDefault,
1661+
* contentFilter: undefined,
1662+
* }
1663+
*/
1664+
Node.getDefaultOptions = function () {
1665+
return {
1666+
enableTypedArray: true,
1667+
isRaw: false,
1668+
qos: QoS.profileDefault,
1669+
contentFilter: undefined,
1670+
};
1671+
};
1672+
16481673
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)