Skip to content

Commit

Permalink
ROS Humble introduced the content-filtering topics feature. This PR
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wayneparrott committed Mar 3, 2023
1 parent feb8e03 commit 6c63a81
Show file tree
Hide file tree
Showing 17 changed files with 1,064 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/windows-build-and-test-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [14.21.2, 16.19.0, 18.14.2, 19.X]
node-version: [14.21.2, 16.19.0, 18.14.1, 19.X]
ros_distribution:
- foxy
- humble
Expand Down
48 changes: 44 additions & 4 deletions docs/EFFICIENCY.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Tips for efficent use of rclnodejs
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.

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.

## Tip-1: Disable Parameter Services

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:

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

## Tip-2: Disable LifecycleNode Lifecycle Services

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:

```
let enableLifecycleCommInterface = false;
let node = new LifecycleNode(
nodeName,
nodeName,
namespace,
Context.defaultContext,
Context.defaultContext,
NodeOptions.defaultOptions,
enableLifecycleCommInterface
enableLifecycleCommInterface
);
```

## Tip-3: Use Content-filtering Subscriptions

The ROS Humble release introduced content-filtering topics
which enable a subscription to limit the messages it receives
to a subset of interest. While the application of the a content-filter
is specific to the DDS/RMW vendor, the general approach is to apply
filtering on the publisher side. This can reduce network bandwidth
for pub-sub communications and message processing and memory
overhead of rclnodejs nodes.

Note: Be sure to confirm that your RMW implementation supports
content-filter before attempting to use it. In cases where content-filtering
is not supported your Subscription will simply ignore your filter and
continue operating with no filtering.

Example:

```
// create a content-filter to limit incoming messages to
// only those with temperature > 75C.
const options = rclnodejs.Node.getDefaultOptions();
options.contentFilter = {
expression: 'temperature > %0',
parameters: [75],
};
node.createSubscription(
'sensor_msgs/msg/Temperature',
'temperature',
options,
(temperatureMsg) => {
console.log(`EMERGENCY temperature detected: ${temperatureMsg.temperature}`);
}
);
```
53 changes: 53 additions & 0 deletions example/publisher-content-filter-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2017 Intel Corporation. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

/* eslint-disable camelcase */

const rclnodejs = require('../index.js');

async function main() {
await rclnodejs.init();
const node = new rclnodejs.Node('publisher_content_filter_example_node');
const publisher = node.createPublisher(
'sensor_msgs/msg/Temperature',
'temperature'
);

let count = 0;
setInterval(function () {
let temperature = (Math.random() * 100).toFixed(2);

publisher.publish({
header: {
stamp: {
sec: 123456,
nanosec: 789,
},
frame_id: 'main frame',
},
temperature: temperature,
variance: 0,
});

console.log(
`Publish temerature message-${++count}: ${temperature} degrees`
);
}, 750);

node.spin();
}

main();
95 changes: 95 additions & 0 deletions example/subscription-content-filter-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) 2023 Wayne Parrott. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

const { assertDefined } = require('dtslint/bin/util.js');
const rclnodejs = require('../index.js');

/**
* This example demonstrates the use of content-filtering
* topics (subscriptions) that were introduced in ROS 2 Humble.
* See the following resources for content-filtering in ROS:
* @see {@link Node#options}
* @see {@link Node#createSubscription}
* @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
*
* Use publisher-content-filter-example.js to generate example messages.
*
* To see all published messages (filterd + unfiltered) run this
* from commandline:
*
* ros2 topic echo temperature
*
* @return {undefined}
*/
async function main() {
await rclnodejs.init();
const node = new rclnodejs.Node('subscription_message_example_node');

let param = 50;

// create a content-filter to limit incoming messages to
// only those with temperature > paramC.
const options = rclnodejs.Node.getDefaultOptions();
options.contentFilter = {
expression: 'temperature > %0',
parameters: [param],
};

let count = 0;
let subscription;
try {
subscription = node.createSubscription(
'sensor_msgs/msg/Temperature',
'temperature',
options,
(temperatureMsg) => {
console.log(`Received temperature message-${++count}:
${temperatureMsg.temperature}C`);
if (count % 5 === 0) {
if (subscription.isContentFilteringEnabled()) {
console.log('Clearing filter');
subscription.clearContentFilter();
} else {
param += 10;
console.log('Update topic content-filter, temperature > ', param);
const contentFilter = {
expression: 'temperature > %0',
parameters: [param],
};
subscription.setContentFilter(contentFilter);
}
console.log(
'Content-filtering enabled: ',
subscription.isContentFilteringEnabled()
);
}
}
);

if (!subscription.isContentFilteringEnabled()) {
console.log('Content-filtering is not enabled on subscription.');
}
} catch (error) {
console.error('Unable to create content-filtering subscription.');
console.error(
'Please ensure your content-filter expression and parameters are well-formed.'
);
}

node.spin();
}

main();
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
'use strict';

const DistroUtils = require('./lib/distro.js');
const RMWUtils = require('./lib/rmw.js');
const { Clock, ROSClock } = require('./lib/clock.js');
const ClockType = require('./lib/clock_type.js');
const compareVersions = require('compare-versions');
Expand Down Expand Up @@ -136,6 +137,9 @@ let rcl = {
/** {@link QoS} class */
QoS: QoS,

/** {@link RMWUtils} */
RMWUtils: RMWUtils,

/** {@link ROSClock} class */
ROSClock: ROSClock,

Expand Down
2 changes: 1 addition & 1 deletion lib/distro.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const DistroUtils = {
* @return {number} Return the rclnodejs distro identifier
*/
getDistroId: function (distroName) {
const dname = distroName ? distroName : this.getDistroName();
const dname = distroName ? distroName.toLowerCase() : this.getDistroName();

return DistroNameIdMap.has(dname)
? DistroNameIdMap.get(dname)
Expand Down
43 changes: 36 additions & 7 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,7 @@ class Node extends rclnodejs.ShadowNode {
}

if (options === undefined) {
options = {
enableTypedArray: true,
isRaw: false,
qos: QoS.profileDefault,
};
return options;
return Node.getDefaultOptions();
}

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

if (options.contentFilter === undefined) {
options = Object.assign(options, { contentFilter: undefined });
}

return options;
}

Expand Down Expand Up @@ -608,7 +607,7 @@ class Node extends rclnodejs.ShadowNode {
*/

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

/**
* Create an Options instance initialized with default values.
* @returns {Options} - The new initialized instance.
* @static
* @example
* {
* enableTypedArray: true,
* isRaw: false,
* qos: QoS.profileDefault,
* contentFilter: undefined,
* }
*/
Node.getDefaultOptions = function () {
return {
enableTypedArray: true,
isRaw: false,
qos: QoS.profileDefault,
contentFilter: undefined,
};
};

module.exports = Node;
29 changes: 29 additions & 0 deletions lib/rmw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const DistroUtils = require('./distro');

const RMWNames = {
FASTRTPS: 'rmw_fastrtps_cpp',
CONNEXT: 'rmw_connext_cpp',
CYCLONEDDS: 'rmw_cyclonedds_cpp',
GURUMDDS: 'rmw_gurumdds_cpp',
};

const DefaultRosRMWNameMap = new Map();
DefaultRosRMWNameMap.set('eloquent', RMWNames.FASTRTPS);
DefaultRosRMWNameMap.set('foxy', RMWNames.FASTRTPS);
DefaultRosRMWNameMap.set('galactic', RMWNames.CYCLONEDDS);
DefaultRosRMWNameMap.set('humble', RMWNames.FASTRTPS);
DefaultRosRMWNameMap.set('rolling', RMWNames.FASTRTPS);

const RMWUtils = {
RMWNames: RMWNames,

getRMWName: function () {
return process.env.RMW_IMPLEMENTATION
? process.env.RMW_IMPLEMENTATION
: DefaultRosRMWNameMap.get(DistroUtils.getDistroName());
},
};

module.exports = RMWUtils;
Loading

0 comments on commit 6c63a81

Please sign in to comment.