Skip to content
Open
50 changes: 47 additions & 3 deletions .github/workflows/linux-x64-build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
# Rolling Ridley (No End-Of-Life)
- docker_image: ubuntu:noble
ros_distribution: rolling
ros_tar_url: "https://github.com/ros2/ros2/releases/download/release-rolling-nightlies/ros2-rolling-nightly-linux-amd64.tar.bz2"
steps:
- name: Setup Node.js ${{ matrix.node-version }} on ${{ matrix.architecture }}
uses: actions/setup-node@v6
Expand All @@ -48,14 +49,46 @@ jobs:
architecture: ${{ matrix.architecture }}

- name: Setup ROS2
if: ${{ matrix.ros_distribution != 'rolling' }}
uses: ros-tooling/setup-ros@v0.7
with:
required-ros-distributions: ${{ matrix.ros_distribution }}
use-ros2-testing: ${{ matrix.ros_distribution == 'rolling' }}

- name: Install ROS2 Rolling Nightly
if: ${{ matrix.ros_distribution == 'rolling' }}
run: |
apt-get update
apt-get install -y software-properties-common curl

# Enable required repositories (per https://docs.ros.org/en/rolling/Installation/Alternatives/Ubuntu-Install-Binary.html)
add-apt-repository universe
ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F'"' '{print $4}')
curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb"
dpkg -i /tmp/ros2-apt-source.deb

# Install prerequisites and apt packages BEFORE nightly tarball
apt-get update
apt-get install -y build-essential cmake tar bzip2 python3 python3-rosdep python3-colcon-common-extensions
apt-get install -y ros-rolling-test-msgs ros-rolling-mrpt-msgs

# Extract nightly binary AFTER apt packages so nightly's newer libs overwrite apt's older ones
curl -sL "${{ matrix.ros_tar_url }}" -o /tmp/ros2-nightly.tar.bz2
mkdir -p /opt/ros/rolling
tar xf /tmp/ros2-nightly.tar.bz2 --strip-components=1 -C /opt/ros/rolling
rm /tmp/ros2-nightly.tar.bz2

# Install any remaining runtime dependencies using rosdep
rosdep init || true
rosdep update
rosdep install --rosdistro rolling --from-paths /opt/ros/rolling/share --ignore-src -y --skip-keys "cyclonedds fastcdr fastdds iceoryx_binding_c rmw_connextdds rti-connext-dds-7.3.0 urdfdom_headers"

- name: Install test-msgs and mrpt_msgs on Linux
if: ${{ matrix.ros_distribution != 'rolling' }}
run: |
sudo apt install -y ros-${{ matrix.ros_distribution }}-test-msgs ros-${{ matrix.ros_distribution }}-mrpt-msgs

- name: Install Electron test dependencies
run: |
sudo apt install ros-${{ matrix.ros_distribution }}-test-msgs ros-${{ matrix.ros_distribution }}-mrpt-msgs
# Adjust dependencies based on Ubuntu version
LIBASOUND_PKG="libasound2"
if grep -q "24.04" /etc/os-release; then
Expand All @@ -65,7 +98,18 @@ jobs:

- uses: actions/checkout@v6

- name: Build and test rclnodejs (nightly)
if: ${{ matrix.ros_distribution == 'rolling' }}
run: |
uname -a
source /opt/ros/rolling/setup.bash
npm i
npm run lint
npm test
npm run clean

- name: Build and test rclnodejs
if: ${{ matrix.ros_distribution != 'rolling' }}
run: |
uname -a
source /opt/ros/${{ matrix.ros_distribution }}/setup.bash
Expand All @@ -77,7 +121,7 @@ jobs:
- name: Test with IDL ROS messages against rolling
if: ${{ matrix.ros_distribution == 'rolling' }}
run: |
source /opt/ros/${{ matrix.ros_distribution }}/setup.bash
source /opt/ros/rolling/setup.bash
npm i
npm run test-idl
npm run clean
Expand Down
13 changes: 13 additions & 0 deletions lib/subscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const rclnodejs = require('./native_loader.js');
const Entity = require('./entity.js');
const DistroUtils = require('./distro.js');
const { applySerializationMode } = require('./message_serialization.js');
const debug = require('debug')('rclnodejs:subscription');

Expand Down Expand Up @@ -144,6 +145,18 @@ class Subscription extends Entity {
return this._serializationMode;
}

/**
* Check if content filtering is supported for this subscription.
* Requires ROS 2 Rolling or later.
* @returns {boolean} True if the subscription instance supports content filtering; otherwise false.
*/
isContentFilterSupported() {
if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
return false;
}
return rclnodejs.isContentFilterSupported(this.handle);
}

/**
* Test if the RMW supports content-filtered topics and that this subscription
* has an active wellformed content-filter.
Expand Down
19 changes: 19 additions & 0 deletions src/rcl_subscription_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include <rcl/error_handling.h>
#include <rcl/rcl.h>
#include <rcl/subscription.h>
#include <rmw/types.h>

#include <cstdio>
Expand Down Expand Up @@ -264,6 +265,20 @@ Napi::Value GetSubscriptionTopic(const Napi::CallbackInfo& info) {
return Napi::String::New(env, topic);
}

#if ROS_VERSION > 2505 // Rolling only
Napi::Value IsContentFilterSupported(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

RclHandle* subscription_handle =
RclHandle::Unwrap(info[0].As<Napi::Object>());
rcl_subscription_t* subscription =
reinterpret_cast<rcl_subscription_t*>(subscription_handle->ptr());

bool is_supported = rcl_subscription_is_cft_supported(subscription);
return Napi::Boolean::New(env, is_supported);
}
#endif
Comment on lines +268 to +280
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preprocessor guard #if ROS_VERSION > 2505 is commented as “Rolling only”, but > 2505 will also evaluate true for any future non-Rolling distro IDs (e.g., 2605, 2705, …). This can (a) cause build failures if rcl_subscription_is_cft_supported() remains Rolling-only, and (b) diverge from the JS runtime guard which only enables the method on DistroId.ROLLING. Consider aligning both sides by guarding on the same threshold (e.g., ROS_VERSION >= 5000 if it’s truly Rolling-only, or updating the JS guard/comment if the API is expected to exist on post-Kilted stable distros too).

Copilot uses AI. Check for mistakes.

Napi::Value HasContentFilter(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

Expand Down Expand Up @@ -476,6 +491,10 @@ Napi::Object InitSubscriptionBindings(Napi::Env env, Napi::Object exports) {
exports.Set("rclTakeRaw", Napi::Function::New(env, RclTakeRaw));
exports.Set("getSubscriptionTopic",
Napi::Function::New(env, GetSubscriptionTopic));
#if ROS_VERSION > 2505 // Rolling only
exports.Set("isContentFilterSupported",
Napi::Function::New(env, IsContentFilterSupported));
#endif
exports.Set("hasContentFilter", Napi::Function::New(env, HasContentFilter));
exports.Set("setContentFilter", Napi::Function::New(env, SetContentFilter));
exports.Set("getContentFilter", Napi::Function::New(env, GetContentFilter));
Expand Down
33 changes: 33 additions & 0 deletions test/test-subscription-content-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,3 +480,36 @@ describe('subscription content-filtering', function () {
done();
});
});

describe('subscription isContentFilterSupported', function () {
this.timeout(30 * 1000);

beforeEach(async function () {
await rclnodejs.init();
this.node = new Node('cft_support_test_node');
});

afterEach(function () {
this.node.destroy();
rclnodejs.shutdown();
});

it('isContentFilterSupported returns boolean matching RMW capability', function (done) {
const typeclass = 'std_msgs/msg/Int16';
const subscription = this.node.createSubscription(
typeclass,
TOPIC,
(msg) => {}
);

const supported = subscription.isContentFilterSupported();
assert.strictEqual(typeof supported, 'boolean');

// isContentFilterSupported requires rolling; on older distros it returns false
const isRolling = DistroUtils.getDistroId() >= DistroUtils.DistroId.ROLLING;
const expectedSupported = isRolling && isContentFilteringSupported();
assert.strictEqual(supported, expectedSupported);

done();
});
});
1 change: 1 addition & 0 deletions test/types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ expectType<rclnodejs.SubscriptionWithRawMessageCallback>(rawMessageCallback);

expectType<string>(subscription.topic);
expectType<boolean>(subscription.isDestroyed());
expectType<boolean>(subscription.isContentFilterSupported());
expectType<boolean>(subscription.setContentFilter(contentFilter));
expectType<rclnodejs.SubscriptionContentFilter | undefined>(
subscription.getContentFilter()
Expand Down
6 changes: 6 additions & 0 deletions types/subscription.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ declare module 'rclnodejs' {
*/
readonly isRaw: boolean;

/**
* Check if content filtering is supported for this subscription.
* @returns True if the subscription instance supports content filtering; otherwise false.
*/
isContentFilterSupported(): boolean;

/**
* Test if the RMW supports content-filtered topics and that this subscription
* is configured with a well formed content-filter.
Expand Down
Loading