diff --git a/lib/event_handler.js b/lib/event_handler.js index b4adb961..9343d0f0 100644 --- a/lib/event_handler.js +++ b/lib/event_handler.js @@ -16,7 +16,11 @@ const rclnodejs = require('./native_loader.js'); const DistroUtils = require('./distro.js'); -const { OperationError } = require('./errors.js'); +const { + OperationError, + RangeValidationError, + TypeValidationError, +} = require('./errors.js'); const Entity = require('./entity.js'); /** @@ -55,6 +59,94 @@ const SubscriptionEventType = { SUBSCRIPTION_MATCHED: 5, }; +/** + * Check if a publisher event type is supported by the active RMW implementation. + * + * Only available in ROS 2 Rolling and later, where the underlying rcl API + * (`rcl_publisher_event_type_is_supported`) is provided. + * + * @param {number} eventType - A {@link PublisherEventType} value. + * @return {boolean} True if the event type is supported by the active RMW + * implementation, false otherwise. + * @throws {OperationError} if invoked on a ROS distro older than Rolling. + * @throws {TypeValidationError} if eventType is not a number. + * @throws {RangeValidationError} if eventType is not a valid + * {@link PublisherEventType} value. + */ +function isPublisherEventTypeSupported(eventType) { + if (typeof rclnodejs.isPublisherEventTypeSupported !== 'function') { + throw new OperationError( + 'isPublisherEventTypeSupported is only available in ROS 2 Rolling and later', + { + code: 'UNSUPPORTED_ROS_VERSION', + entityType: 'publisher event type', + details: { + requiredVersion: 'rolling', + currentVersion: DistroUtils.getDistroId(), + }, + } + ); + } + if (typeof eventType !== 'number') { + throw new TypeValidationError('eventType', eventType, 'number', { + entityType: 'publisher event type', + }); + } + if (!Object.values(PublisherEventType).includes(eventType)) { + throw new RangeValidationError( + 'eventType', + eventType, + 'one of PublisherEventType values', + { entityType: 'publisher event type' } + ); + } + return rclnodejs.isPublisherEventTypeSupported(eventType); +} + +/** + * Check if a subscription event type is supported by the active RMW implementation. + * + * Only available in ROS 2 Rolling and later, where the underlying rcl API + * (`rcl_subscription_event_type_is_supported`) is provided. + * + * @param {number} eventType - A {@link SubscriptionEventType} value. + * @return {boolean} True if the event type is supported by the active RMW + * implementation, false otherwise. + * @throws {OperationError} if invoked on a ROS distro older than Rolling. + * @throws {TypeValidationError} if eventType is not a number. + * @throws {RangeValidationError} if eventType is not a valid + * {@link SubscriptionEventType} value. + */ +function isSubscriptionEventTypeSupported(eventType) { + if (typeof rclnodejs.isSubscriptionEventTypeSupported !== 'function') { + throw new OperationError( + 'isSubscriptionEventTypeSupported is only available in ROS 2 Rolling and later', + { + code: 'UNSUPPORTED_ROS_VERSION', + entityType: 'subscription event type', + details: { + requiredVersion: 'rolling', + currentVersion: DistroUtils.getDistroId(), + }, + } + ); + } + if (typeof eventType !== 'number') { + throw new TypeValidationError('eventType', eventType, 'number', { + entityType: 'subscription event type', + }); + } + if (!Object.values(SubscriptionEventType).includes(eventType)) { + throw new RangeValidationError( + 'eventType', + eventType, + 'one of SubscriptionEventType values', + { entityType: 'subscription event type' } + ); + } + return rclnodejs.isSubscriptionEventTypeSupported(eventType); +} + class EventHandler extends Entity { constructor(handle, callback, eventType, eventTypeName) { super(handle, null, null); @@ -488,4 +580,6 @@ module.exports = { PublisherEventType, SubscriptionEventCallbacks, SubscriptionEventType, + isPublisherEventTypeSupported, + isSubscriptionEventTypeSupported, }; diff --git a/src/rcl_event_handle_bindings.cpp b/src/rcl_event_handle_bindings.cpp index 46705013..e497c5e4 100644 --- a/src/rcl_event_handle_bindings.cpp +++ b/src/rcl_event_handle_bindings.cpp @@ -247,6 +247,26 @@ Napi::Value CreatePublisherEventHandle(const Napi::CallbackInfo& info) { return js_obj; } +#if ROS_VERSION >= 5000 +Napi::Value IsPublisherEventTypeSupported(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + rcl_publisher_event_type_t event_type = + static_cast( + info[0].As().Int32Value()); + return Napi::Boolean::New(env, + rcl_publisher_event_type_is_supported(event_type)); +} + +Napi::Value IsSubscriptionEventTypeSupported(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + rcl_subscription_event_type_t event_type = + static_cast( + info[0].As().Int32Value()); + return Napi::Boolean::New( + env, rcl_subscription_event_type_is_supported(event_type)); +} +#endif // ROS_VERSION >= 5000 + Napi::Value TakeEvent(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); RclHandle* event_handle = RclHandle::Unwrap(info[0].As()); @@ -288,6 +308,12 @@ Napi::Object InitEventHandleBindings(Napi::Env env, Napi::Object exports) { exports.Set("createPublisherEventHandle", Napi::Function::New(env, CreatePublisherEventHandle)); exports.Set("takeEvent", Napi::Function::New(env, TakeEvent)); +#if ROS_VERSION >= 5000 + exports.Set("isPublisherEventTypeSupported", + Napi::Function::New(env, IsPublisherEventTypeSupported)); + exports.Set("isSubscriptionEventTypeSupported", + Napi::Function::New(env, IsSubscriptionEventTypeSupported)); +#endif // ROS_VERSION >= 5000 return exports; } diff --git a/test/test-event-handle.js b/test/test-event-handle.js index e7dcfedc..b80c2703 100644 --- a/test/test-event-handle.js +++ b/test/test-event-handle.js @@ -24,6 +24,8 @@ const { PublisherEventCallbacks, PublisherEventType, SubscriptionEventType, + isPublisherEventTypeSupported, + isSubscriptionEventTypeSupported, } = require('../lib/event_handler.js'); describe('Event handle test suite prior to jazzy', function () { @@ -46,6 +48,87 @@ describe('Event handle test suite prior to jazzy', function () { }); }); +describe('Event type is supported - native binding unavailable', function () { + before(function () { + if ( + typeof rclnodejsBinding.isPublisherEventTypeSupported === 'function' && + typeof rclnodejsBinding.isSubscriptionEventTypeSupported === 'function' + ) { + this.skip(); + } + }); + + it('isPublisherEventTypeSupported throws when native binding is missing', function () { + assert.throws(() => { + isPublisherEventTypeSupported(PublisherEventType.PUBLISHER_MATCHED); + }, /isPublisherEventTypeSupported is only available in ROS 2 Rolling and later/); + }); + + it('isSubscriptionEventTypeSupported throws when native binding is missing', function () { + assert.throws(() => { + isSubscriptionEventTypeSupported( + SubscriptionEventType.SUBSCRIPTION_MATCHED + ); + }, /isSubscriptionEventTypeSupported is only available in ROS 2 Rolling and later/); + }); +}); + +describe('Event type is supported - native binding available', function () { + before(function () { + if ( + typeof rclnodejsBinding.isPublisherEventTypeSupported !== 'function' || + typeof rclnodejsBinding.isSubscriptionEventTypeSupported !== 'function' + ) { + this.skip(); + } + }); + + it('isPublisherEventTypeSupported returns a boolean for every event type', function () { + for (const eventType of Object.values(PublisherEventType)) { + const result = isPublisherEventTypeSupported(eventType); + assert.strictEqual(typeof result, 'boolean'); + } + }); + + it('isSubscriptionEventTypeSupported returns a boolean for every event type', function () { + for (const eventType of Object.values(SubscriptionEventType)) { + const result = isSubscriptionEventTypeSupported(eventType); + assert.strictEqual(typeof result, 'boolean'); + } + }); + + it('MATCHED events are reported as supported across RMW implementations', function () { + assert.strictEqual( + isPublisherEventTypeSupported(PublisherEventType.PUBLISHER_MATCHED), + true + ); + assert.strictEqual( + isSubscriptionEventTypeSupported( + SubscriptionEventType.SUBSCRIPTION_MATCHED + ), + true + ); + }); + + it('isPublisherEventTypeSupported rejects invalid event types', function () { + assert.throws(() => { + isPublisherEventTypeSupported(-1); + }, /Value '-1' for 'eventType' is out of range: one of PublisherEventType values/); + assert.throws(() => { + isPublisherEventTypeSupported('matched'); + }, /Invalid type for 'eventType': expected number, got string/); + }); + + it('isSubscriptionEventTypeSupported rejects invalid event types', function () { + assert.throws(() => { + isSubscriptionEventTypeSupported(999); + }, /Value '999' for 'eventType' is out of range: one of SubscriptionEventType values/); + assert.throws(() => { + isSubscriptionEventTypeSupported(undefined); + }, /Invalid type for 'eventType': expected number, got undefined/); + }); +}); + describe('Event handle test suite', function () { this.timeout(5 * 1000); let node;