diff --git a/.github/workflows/linux-x64-build-and-test.yml b/.github/workflows/linux-x64-build-and-test.yml index 86288ab5d..3805bed65 100644 --- a/.github/workflows/linux-x64-build-and-test.yml +++ b/.github/workflows/linux-x64-build-and-test.yml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/subscription.js b/lib/subscription.js index 4042bb3cb..8cd3a2149 100644 --- a/lib/subscription.js +++ b/lib/subscription.js @@ -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'); @@ -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. diff --git a/src/rcl_subscription_bindings.cpp b/src/rcl_subscription_bindings.cpp index c7bdffd91..078e89abf 100644 --- a/src/rcl_subscription_bindings.cpp +++ b/src/rcl_subscription_bindings.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -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()); + rcl_subscription_t* subscription = + reinterpret_cast(subscription_handle->ptr()); + + bool is_supported = rcl_subscription_is_cft_supported(subscription); + return Napi::Boolean::New(env, is_supported); +} +#endif + Napi::Value HasContentFilter(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); @@ -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)); diff --git a/test/test-subscription-content-filter.js b/test/test-subscription-content-filter.js index 099b4ae15..67f6b9668 100644 --- a/test/test-subscription-content-filter.js +++ b/test/test-subscription-content-filter.js @@ -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(); + }); +}); diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index f125b5c2b..a908bbf6a 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -245,6 +245,7 @@ expectType(rawMessageCallback); expectType(subscription.topic); expectType(subscription.isDestroyed()); +expectType(subscription.isContentFilterSupported()); expectType(subscription.setContentFilter(contentFilter)); expectType( subscription.getContentFilter() diff --git a/types/subscription.d.ts b/types/subscription.d.ts index 5fdf7b825..44dddd1c8 100644 --- a/types/subscription.d.ts +++ b/types/subscription.d.ts @@ -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.