Skip to content
Closed
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: [10.X, 12.X, 14.X, 16.11.X, 17.X, 18.X, 19.X]
node-version: [10.X, 12.X, 14.X, 16.11.X, 17.X, 18.12.X, 19.X]
ros_distribution:
# - foxy
- galactic
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/windows-build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [10.X, 12.X, 14.X, 16.11.X, 17.X, 18.X, 19.X]
node-version: [10.X, 12.X, 14.X, 16.11.X, 17.X, 18.12.0, 19.X]
steps:
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
Expand Down
51 changes: 51 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Create an image configured with ROS2 including colcon, Nodejs and rclnodejs source
# Supported ARGS:
# ROS_DISTRO = [foxy, galactic, humble, rolling], default=rolling
# NODE_MAJOR_VER = [12, 14, 16, 18, 19], default=19
# BRANCH = rclnodejs git branch, default=develop
#
# examples:
#
# Build image named 'rclnodejs' and run it with the rclnode test suite
#
# docker build -t rclnodejs .
# docker run -it rclnodejs npm test
#
#
# Build an image for a specific branch of rclnodejs, version of ROS2 and Nodejs use:
#
# docker build -t <image_name> --build-arg DISTRO=galactic .
# docker build -t <image_name> \
# --build-arg ROS_DISTRO=humble \
# --build-arg BRANCH=humble-hawksbill \
# --build-arg NODE_MAJOR_VER=18 .
#
#
# Build and run:
# docker run -it --rm $(docker build -q .)
#

# use
ARG ROS_DISTRO=rolling
FROM ros:${ROS_DISTRO}

# Install dependencies, including Nodejs
ARG NODE_MAJOR_VER=19
RUN apt-get update -y \
&& apt-get install -y curl sudo \
&& curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR_VER}.x | sudo -E bash - \
&& apt-get install -y nodejs

# clone a branch of the rclnodejs repo, build addon libs, generate corresponding JS msgs
ARG BRANCH=develop
WORKDIR /rosdev
SHELL ["/bin/bash", "-c"]
RUN source /opt/ros/${ROS_DISTRO}/setup.bash \
&& apt install ros-${ROS_DISTRO}-test-msgs \
&& apt install ros-${ROS_DISTRO}-example-interfaces \
&& git clone -b ${BRANCH} --single-branch https://github.com/RobotWebTools/rclnodejs.git \
&& cd /rosdev/rclnodejs \
&& npm i

WORKDIR /rosdev/rclnodejs
CMD [ "bash" ]
42 changes: 38 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,48 @@ 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. Because content-filtering is performed
at the RMW level, rclnodejs message-processing will never receive
unwanted messages. This feature can greatly reduce the message
processing and memory overhead of your nodes.

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();
71 changes: 71 additions & 0 deletions example/subscription-content-filter-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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');

// 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],
};

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`);
}
);
} 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
5 changes: 3 additions & 2 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ class Client extends Entity {
this._sequenceNumberToCallbackMap.delete(sequenceNumber);
callback(response.toPlainObject(this.typedArrayEnabled));
} else {
error(`Client has received an unexpected ${this._serviceName}
response with sequence number ${sequenceNumber}.`);
throw new Error(
`Client has received an unexpected ${this._serviceName} with sequence number ${sequenceNumber}.`
);
}
}

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
30 changes: 23 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,17 @@ 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.
* @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|DDS 1.4 specification, Annex B}
*/
createSubscription(typeClass, topic, options, callback) {
if (typeof typeClass === 'string' || typeof typeClass === 'object') {
Expand Down Expand Up @@ -1645,4 +1652,13 @@ class Node extends rclnodejs.ShadowNode {
}
}

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