Skip to content

Commit 3a57845

Browse files
authored
Add support check for content filter feature in subscription (#1451)
Corresponding to rclpy [d892204](ros2/rclpy@d892204) and rcl's `rcl_subscription_is_cft_supported()` API, this adds `isContentFilterSupported()` to the `Subscription` class. This allows users to query at runtime whether the underlying RMW supports content-filtered topics for a given subscription, rather than relying on hardcoded RMW name checks. Currently gated to ROS 2 Rolling only (`ROS_VERSION > 2505`); on older distros it returns `false`. ### Feature changes - **src/rcl_subscription_bindings.cpp** — Added `IsContentFilterSupported()` N-API function calling `rcl_subscription_is_cft_supported()`, guarded by `#if ROS_VERSION > 2505`. Added `#include <rcl/subscription.h>`. Registered as `"isContentFilterSupported"` in `InitSubscriptionBindings()`. - **lib/subscription.js** — Added `isContentFilterSupported()` method with a distro guard (`DistroUtils.getDistroId() < ROLLING` returns `false`). Added `DistroUtils` import. - **types/subscription.d.ts** — Added `isContentFilterSupported(): boolean` to the `Subscription` interface. - **test/test-subscription-content-filter.js** — Added a separate top-level `describe('subscription isContentFilterSupported')` block that runs on all distros. Validates the return value accounts for both the rolling requirement and RMW capability. - **test/types/index.test-d.ts** — Added `expectType<boolean>` assertion for `isContentFilterSupported()`. ### CI changes - **.github/workflows/linux-x64-build-and-test.yml** — Replaced `setup-ros` with `use-ros2-testing` for rolling with a nightly binary tarball approach, following the [official ROS 2 Rolling binary installation guide](https://docs.ros.org/en/rolling/Installation/Alternatives/Ubuntu-Install-Binary.html): - Configures ROS apt sources via `ros2-apt-source` deb package. - Installs apt packages (`test-msgs`, `mrpt-msgs`, `colcon`, `rosdep`) **before** extracting the nightly tarball, so the nightly's newer libs (e.g. `librcutils.so` with `rcutils_decode_base64`) overwrite apt's older versions. - Extracts nightly tarball with `--strip-components=1` into `/opt/ros/rolling/` so everything lives under one prefix — single `source /opt/ros/rolling/setup.bash` like all other distros. - Runs `rosdep install` for remaining runtime dependencies. - Separated Electron test dependencies (`xvfb`, `libgtk-3-0`, etc.) into its own step that runs for all distros. - Added `-y` to `apt install` for test-msgs. - Removed stale `use-ros2-testing` parameter from `setup-ros`. Fix: #1450
1 parent bf8e893 commit 3a57845

6 files changed

Lines changed: 119 additions & 3 deletions

File tree

.github/workflows/linux-x64-build-and-test.yml

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jobs:
4040
# Rolling Ridley (No End-Of-Life)
4141
- docker_image: ubuntu:noble
4242
ros_distribution: rolling
43+
ros_tar_url: "https://github.com/ros2/ros2/releases/download/release-rolling-nightlies/ros2-rolling-nightly-linux-amd64.tar.bz2"
4344
steps:
4445
- name: Setup Node.js ${{ matrix.node-version }} on ${{ matrix.architecture }}
4546
uses: actions/setup-node@v6
@@ -48,14 +49,46 @@ jobs:
4849
architecture: ${{ matrix.architecture }}
4950

5051
- name: Setup ROS2
52+
if: ${{ matrix.ros_distribution != 'rolling' }}
5153
uses: ros-tooling/setup-ros@v0.7
5254
with:
5355
required-ros-distributions: ${{ matrix.ros_distribution }}
54-
use-ros2-testing: ${{ matrix.ros_distribution == 'rolling' }}
56+
57+
- name: Install ROS2 Rolling Nightly
58+
if: ${{ matrix.ros_distribution == 'rolling' }}
59+
run: |
60+
apt-get update
61+
apt-get install -y software-properties-common curl
62+
63+
# Enable required repositories (per https://docs.ros.org/en/rolling/Installation/Alternatives/Ubuntu-Install-Binary.html)
64+
add-apt-repository universe
65+
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}')
66+
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"
67+
dpkg -i /tmp/ros2-apt-source.deb
68+
69+
# Install prerequisites and apt packages BEFORE nightly tarball
70+
apt-get update
71+
apt-get install -y build-essential cmake tar bzip2 python3 python3-rosdep python3-colcon-common-extensions
72+
apt-get install -y ros-rolling-test-msgs ros-rolling-mrpt-msgs
73+
74+
# Extract nightly binary AFTER apt packages so nightly's newer libs overwrite apt's older ones
75+
curl -sL "${{ matrix.ros_tar_url }}" -o /tmp/ros2-nightly.tar.bz2
76+
mkdir -p /opt/ros/rolling
77+
tar xf /tmp/ros2-nightly.tar.bz2 --strip-components=1 -C /opt/ros/rolling
78+
rm /tmp/ros2-nightly.tar.bz2
79+
80+
# Install any remaining runtime dependencies using rosdep
81+
rosdep init || true
82+
rosdep update
83+
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"
5584
5685
- name: Install test-msgs and mrpt_msgs on Linux
86+
if: ${{ matrix.ros_distribution != 'rolling' }}
87+
run: |
88+
sudo apt install -y ros-${{ matrix.ros_distribution }}-test-msgs ros-${{ matrix.ros_distribution }}-mrpt-msgs
89+
90+
- name: Install Electron test dependencies
5791
run: |
58-
sudo apt install ros-${{ matrix.ros_distribution }}-test-msgs ros-${{ matrix.ros_distribution }}-mrpt-msgs
5992
# Adjust dependencies based on Ubuntu version
6093
LIBASOUND_PKG="libasound2"
6194
if grep -q "24.04" /etc/os-release; then
@@ -65,7 +98,18 @@ jobs:
6598
6699
- uses: actions/checkout@v6
67100

101+
- name: Build and test rclnodejs (nightly)
102+
if: ${{ matrix.ros_distribution == 'rolling' }}
103+
run: |
104+
uname -a
105+
source /opt/ros/rolling/setup.bash
106+
npm i
107+
npm run lint
108+
npm test
109+
npm run clean
110+
68111
- name: Build and test rclnodejs
112+
if: ${{ matrix.ros_distribution != 'rolling' }}
69113
run: |
70114
uname -a
71115
source /opt/ros/${{ matrix.ros_distribution }}/setup.bash
@@ -77,7 +121,7 @@ jobs:
77121
- name: Test with IDL ROS messages against rolling
78122
if: ${{ matrix.ros_distribution == 'rolling' }}
79123
run: |
80-
source /opt/ros/${{ matrix.ros_distribution }}/setup.bash
124+
source /opt/ros/rolling/setup.bash
81125
npm i
82126
npm run test-idl
83127
npm run clean

lib/subscription.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
const rclnodejs = require('./native_loader.js');
1818
const Entity = require('./entity.js');
19+
const DistroUtils = require('./distro.js');
1920
const { applySerializationMode } = require('./message_serialization.js');
2021
const debug = require('debug')('rclnodejs:subscription');
2122

@@ -144,6 +145,18 @@ class Subscription extends Entity {
144145
return this._serializationMode;
145146
}
146147

148+
/**
149+
* Check if content filtering is supported for this subscription.
150+
* Requires ROS 2 Rolling or later.
151+
* @returns {boolean} True if the subscription instance supports content filtering; otherwise false.
152+
*/
153+
isContentFilterSupported() {
154+
if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
155+
return false;
156+
}
157+
return rclnodejs.isContentFilterSupported(this.handle);
158+
}
159+
147160
/**
148161
* Test if the RMW supports content-filtered topics and that this subscription
149162
* has an active wellformed content-filter.

src/rcl_subscription_bindings.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <rcl/error_handling.h>
1818
#include <rcl/rcl.h>
19+
#include <rcl/subscription.h>
1920
#include <rmw/types.h>
2021

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

268+
#if ROS_VERSION > 2505 // Rolling only
269+
Napi::Value IsContentFilterSupported(const Napi::CallbackInfo& info) {
270+
Napi::Env env = info.Env();
271+
272+
RclHandle* subscription_handle =
273+
RclHandle::Unwrap(info[0].As<Napi::Object>());
274+
rcl_subscription_t* subscription =
275+
reinterpret_cast<rcl_subscription_t*>(subscription_handle->ptr());
276+
277+
bool is_supported = rcl_subscription_is_cft_supported(subscription);
278+
return Napi::Boolean::New(env, is_supported);
279+
}
280+
#endif
281+
267282
Napi::Value HasContentFilter(const Napi::CallbackInfo& info) {
268283
Napi::Env env = info.Env();
269284

@@ -476,6 +491,10 @@ Napi::Object InitSubscriptionBindings(Napi::Env env, Napi::Object exports) {
476491
exports.Set("rclTakeRaw", Napi::Function::New(env, RclTakeRaw));
477492
exports.Set("getSubscriptionTopic",
478493
Napi::Function::New(env, GetSubscriptionTopic));
494+
#if ROS_VERSION > 2505 // Rolling only
495+
exports.Set("isContentFilterSupported",
496+
Napi::Function::New(env, IsContentFilterSupported));
497+
#endif
479498
exports.Set("hasContentFilter", Napi::Function::New(env, HasContentFilter));
480499
exports.Set("setContentFilter", Napi::Function::New(env, SetContentFilter));
481500
exports.Set("getContentFilter", Napi::Function::New(env, GetContentFilter));

test/test-subscription-content-filter.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,36 @@ describe('subscription content-filtering', function () {
480480
done();
481481
});
482482
});
483+
484+
describe('subscription isContentFilterSupported', function () {
485+
this.timeout(30 * 1000);
486+
487+
beforeEach(async function () {
488+
await rclnodejs.init();
489+
this.node = new Node('cft_support_test_node');
490+
});
491+
492+
afterEach(function () {
493+
this.node.destroy();
494+
rclnodejs.shutdown();
495+
});
496+
497+
it('isContentFilterSupported returns boolean matching RMW capability', function (done) {
498+
const typeclass = 'std_msgs/msg/Int16';
499+
const subscription = this.node.createSubscription(
500+
typeclass,
501+
TOPIC,
502+
(msg) => {}
503+
);
504+
505+
const supported = subscription.isContentFilterSupported();
506+
assert.strictEqual(typeof supported, 'boolean');
507+
508+
// isContentFilterSupported requires rolling; on older distros it returns false
509+
const isRolling = DistroUtils.getDistroId() >= DistroUtils.DistroId.ROLLING;
510+
const expectedSupported = isRolling && isContentFilteringSupported();
511+
assert.strictEqual(supported, expectedSupported);
512+
513+
done();
514+
});
515+
});

test/types/index.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ expectType<rclnodejs.SubscriptionWithRawMessageCallback>(rawMessageCallback);
245245

246246
expectType<string>(subscription.topic);
247247
expectType<boolean>(subscription.isDestroyed());
248+
expectType<boolean>(subscription.isContentFilterSupported());
248249
expectType<boolean>(subscription.setContentFilter(contentFilter));
249250
expectType<rclnodejs.SubscriptionContentFilter | undefined>(
250251
subscription.getContentFilter()

types/subscription.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ declare module 'rclnodejs' {
5151
*/
5252
readonly isRaw: boolean;
5353

54+
/**
55+
* Check if content filtering is supported for this subscription.
56+
* @returns True if the subscription instance supports content filtering; otherwise false.
57+
*/
58+
isContentFilterSupported(): boolean;
59+
5460
/**
5561
* Test if the RMW supports content-filtered topics and that this subscription
5662
* is configured with a well formed content-filter.

0 commit comments

Comments
 (0)