Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing content-filtering topics #901

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) 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';

/* 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.hasContentFilter()) {
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.hasContentFilter()
);
}
}
);

if (!subscription.hasContentFilter()) {
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
39 changes: 32 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 Down Expand Up @@ -608,7 +603,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 +612,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 +1649,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