diff --git a/binding.gyp b/binding.gyp index ce422a63..18c41cf7 100644 --- a/binding.gyp +++ b/binding.gyp @@ -173,6 +173,7 @@ # After Humble, e.g., Jazzy, Kilted. 'ros_version > 2205', { 'sources': [ + './src/rcl_event_handle_bindings.cpp', './src/rcl_type_description_service_bindings.cpp', ] } diff --git a/lib/event_handler.js b/lib/event_handler.js new file mode 100644 index 00000000..585bdf26 --- /dev/null +++ b/lib/event_handler.js @@ -0,0 +1,474 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// 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 rclnodejs = require('bindings')('rclnodejs'); +const DistroUtils = require('./distro.js'); +const Entity = require('./entity.js'); + +/** + * Enumeration for PublisherEventCallbacks event types. + * @enum {number} + */ +const PublisherEventType = { + /** @member {number} */ + PUBLISHER_OFFERED_DEADLINE_MISSED: 0, + /** @member {number} */ + PUBLISHER_LIVELINESS_LOST: 1, + /** @member {number} */ + PUBLISHER_OFFERED_INCOMPATIBLE_QOS: 2, + /** @member {number} */ + PUBLISHER_INCOMPATIBLE_TYPE: 3, + /** @member {number} */ + PUBLISHER_MATCHED: 4, +}; + +/** + * Enumeration for SubscriptionEventCallbacks event types. + * @enum {number} + */ +const SubscriptionEventType = { + /** @member {number} */ + SUBSCRIPTION_REQUESTED_DEADLINE_MISSED: 0, + /** @member {number} */ + SUBSCRIPTION_LIVELINESS_CHANGED: 1, + /** @member {number} */ + SUBSCRIPTION_REQUESTED_INCOMPATIBLE_QOS: 2, + /** @member {number} */ + SUBSCRIPTION_MESSAGE_LOST: 3, + /** @member {number} */ + SUBSCRIPTION_INCOMPATIBLE_TYPE: 4, + /** @member {number} */ + SUBSCRIPTION_MATCHED: 5, +}; + +class EventHandler extends Entity { + constructor(handle, callback, eventType, eventTypeName) { + super(handle, null, null); + this._callback = callback; + this._eventType = eventType; + this._eventTypeName = eventTypeName; + } + + takeData() { + const data = rclnodejs.takeEvent(this._handle, { + [this._eventTypeName]: this._eventType, + }); + if (this._callback) { + this._callback(data); + } + } +} + +/** + * @class - Class representing a ROS 2 PublisherEventCallbacks + * @hideconstructor + */ +class PublisherEventCallbacks { + constructor() { + if (DistroUtils.getDistroId() < DistroUtils.getDistroId('jazzy')) { + throw new Error( + 'PublisherEventCallbacks is only available in ROS 2 Jazzy and later.' + ); + } + this._deadline = null; + this._incompatible_qos = null; + this._liveliness = null; + this._incompatible_type = null; + this._matched = null; + this._eventHandlers = []; + } + + /** + * Set deadline missed callback. + * @param {function} callback - The callback function to be called. + */ + set deadline(callback) { + this._deadline = callback; + } + + /** + * Get deadline missed callback. + * @return {function} - The callback function. + */ + get deadline() { + return this._deadline; + } + + /** + * Set incompatible QoS callback. + * @param {function} callback - The callback function to be called. + */ + set incompatibleQos(callback) { + this._incompatible_qos = callback; + } + + /** + * Get incompatible QoS callback. + * @return {function} - The callback function. + */ + get incompatibleQos() { + return this._incompatible_qos; + } + + /** + * Set liveliness lost callback. + * @param {function} callback - The callback function to be called. + */ + set liveliness(callback) { + this._liveliness = callback; + } + + /** + * Get liveliness lost callback. + * @return {function} - The callback function. + */ + get liveliness() { + return this._liveliness; + } + + /** + * Set incompatible type callback. + * @param {function} callback - The callback function to be called. + */ + set incompatibleType(callback) { + this._incompatible_type = callback; + } + + /** + * Get incompatible type callback. + * @return {function} - The callback function. + */ + get incompatibleType() { + return this._incompatible_type; + } + + /** + * Set matched callback. + * @param {function} callback - The callback function to be called. + */ + set matched(callback) { + this._matched = callback; + } + + /** + * Get matched callback. + * @return {function} - The callback function. + */ + get matched() { + return this._matched; + } + + createEventHandlers(publisherHandle) { + if (this._deadline) { + const deadlineHandle = rclnodejs.createPublisherEventHandle( + publisherHandle, + PublisherEventType.PUBLISHER_OFFERED_DEADLINE_MISSED + ); + this._eventHandlers.push( + new EventHandler( + deadlineHandle, + this._deadline, + PublisherEventType.PUBLISHER_OFFERED_DEADLINE_MISSED, + 'publisher_event_type' + ) + ); + } + + if (this._incompatible_qos) { + const incompatibleQosHandle = rclnodejs.createPublisherEventHandle( + publisherHandle, + PublisherEventType.PUBLISHER_OFFERED_INCOMPATIBLE_QOS + ); + this._eventHandlers.push( + new EventHandler( + incompatibleQosHandle, + this._incompatible_qos, + PublisherEventType.PUBLISHER_OFFERED_INCOMPATIBLE_QOS, + 'publisher_event_type' + ) + ); + } + + if (this._liveliness) { + const livelinessHandle = rclnodejs.createPublisherEventHandle( + publisherHandle, + PublisherEventType.PUBLISHER_LIVELINESS_LOST + ); + this._eventHandlers.push( + new EventHandler( + livelinessHandle, + this._liveliness, + PublisherEventType.PUBLISHER_LIVELINESS_LOST, + 'publisher_event_type' + ) + ); + } + + if (this._incompatible_type) { + const incompatibleTypeHandle = rclnodejs.createPublisherEventHandle( + publisherHandle, + PublisherEventType.PUBLISHER_INCOMPATIBLE_TYPE + ); + this._eventHandlers.push( + new EventHandler( + incompatibleTypeHandle, + this._incompatible_type, + PublisherEventType.PUBLISHER_INCOMPATIBLE_TYPE, + 'publisher_event_type' + ) + ); + } + + if (this._matched) { + const matchedHandle = rclnodejs.createPublisherEventHandle( + publisherHandle, + PublisherEventType.PUBLISHER_MATCHED + ); + this._eventHandlers.push( + new EventHandler( + matchedHandle, + this._matched, + PublisherEventType.PUBLISHER_MATCHED, + 'publisher_event_type' + ) + ); + } + + return this._eventHandlers; + } + + get eventHandlers() { + return this._eventHandlers; + } +} + +/** + * @class - Class representing a ROS 2 SubscriptionEventCallbacks + * @hideconstructor + */ +class SubscriptionEventCallbacks { + constructor() { + if (DistroUtils.getDistroId() < DistroUtils.getDistroId('jazzy')) { + throw new Error( + 'SubscriptionEventCallbacks is only available in ROS 2 Jazzy and later.' + ); + } + this._deadline = null; + this._incompatible_qos = null; + this._liveliness = null; + this._message_lost = null; + this._incompatible_type = null; + this._matched = null; + this._eventHandlers = []; + } + + /** + * Set the callback for deadline missed event. + * @param {function} callback - The callback function to be called. + */ + set deadline(callback) { + this._deadline = callback; + } + + /** + * Get the callback for deadline missed event. + * @return {function} - The callback function. + */ + get deadline() { + return this._deadline; + } + + /** + * Set the callback for incompatible QoS event. + * @param {function} callback - The callback function to be called. + */ + set incompatibleQos(callback) { + this._incompatible_qos = callback; + } + + /** + * Get the callback for incompatible QoS event. + * @return {function} - The callback function. + */ + get incompatibleQos() { + return this._incompatible_qos; + } + + /** + * Set the callback for liveliness changed event. + * @param {function} callback - The callback function to be called. + */ + set liveliness(callback) { + this._liveliness = callback; + } + + /** + * Get the callback for liveliness changed event. + * @return {function} - The callback function. + */ + get liveliness() { + return this._liveliness; + } + + /** + * Set the callback for message lost event. + * @param {function} callback - The callback function to be called. + */ + set messageLost(callback) { + this._message_lost = callback; + } + + /** + * Get the callback for message lost event. + * @return {function} - The callback function. + */ + get messageLost() { + return this._message_lost; + } + + /** + * Set the callback for incompatible type event. + * @param {function} callback - The callback function to be called. + */ + set incompatibleType(callback) { + this._incompatible_type = callback; + } + + /** + * Get the callback for incompatible type event. + * @return {function} - The callback function. + */ + get incompatibleType() { + return this._incompatible_type; + } + + /** + * Set the callback for matched event. + * @param {function} callback - The callback function to be called. + */ + set matched(callback) { + this._matched = callback; + } + + /** + * Get the callback for matched event. + * @return {function} - The callback function. + */ + get matched() { + return this._matched; + } + + createEventHandlers(subscriptionHandle) { + if (this._deadline) { + const deadlineHandle = rclnodejs.createSubscriptionEventHandle( + subscriptionHandle, + SubscriptionEventType.SUBSCRIPTION_REQUESTED_DEADLINE_MISSED + ); + this._eventHandlers.push( + new EventHandler( + deadlineHandle, + this._deadline, + SubscriptionEventType.SUBSCRIPTION_REQUESTED_DEADLINE_MISSED, + 'subscription_event_type' + ) + ); + } + + if (this._incompatible_qos) { + const incompatibleQosHandle = rclnodejs.createSubscriptionEventHandle( + subscriptionHandle, + SubscriptionEventType.SUBSCRIPTION_REQUESTED_INCOMPATIBLE_QOS + ); + this._eventHandlers.push( + new EventHandler( + incompatibleQosHandle, + this._incompatible_qos, + SubscriptionEventType.SUBSCRIPTION_REQUESTED_INCOMPATIBLE_QOS, + 'subscription_event_type' + ) + ); + } + + if (this._liveliness) { + const livelinessHandle = rclnodejs.createSubscriptionEventHandle( + subscriptionHandle, + SubscriptionEventType.SUBSCRIPTION_LIVELINESS_CHANGED + ); + this._eventHandlers.push( + new EventHandler( + livelinessHandle, + this._liveliness, + SubscriptionEventType.SUBSCRIPTION_LIVELINESS_CHANGED, + 'subscription_event_type' + ) + ); + } + + if (this._message_lost) { + const messageLostHandle = rclnodejs.createSubscriptionEventHandle( + subscriptionHandle, + SubscriptionEventType.SUBSCRIPTION_MESSAGE_LOST + ); + this._eventHandlers.push( + new EventHandler( + messageLostHandle, + this._message_lost, + SubscriptionEventType.SUBSCRIPTION_MESSAGE_LOST, + 'subscription_event_type' + ) + ); + } + + if (this._incompatible_type) { + const incompatibleTypeHandle = rclnodejs.createSubscriptionEventHandle( + subscriptionHandle, + SubscriptionEventType.SUBSCRIPTION_INCOMPATIBLE_TYPE + ); + this._eventHandlers.push( + new EventHandler( + incompatibleTypeHandle, + this._incompatible_type, + SubscriptionEventType.SUBSCRIPTION_INCOMPATIBLE_TYPE, + 'subscription_event_type' + ) + ); + } + + if (this._matched) { + const matchedHandle = rclnodejs.createSubscriptionEventHandle( + subscriptionHandle, + SubscriptionEventType.SUBSCRIPTION_MATCHED + ); + this._eventHandlers.push( + new EventHandler( + matchedHandle, + this._matched, + SubscriptionEventType.SUBSCRIPTION_MATCHED, + 'subscription_event_type' + ) + ); + } + + return this._eventHandlers; + } +} + +module.exports = { + PublisherEventCallbacks, + PublisherEventType, + SubscriptionEventCallbacks, + SubscriptionEventType, +}; diff --git a/lib/lifecycle_publisher.js b/lib/lifecycle_publisher.js index 34964300..b004109c 100644 --- a/lib/lifecycle_publisher.js +++ b/lib/lifecycle_publisher.js @@ -93,10 +93,10 @@ class LifecyclePublisher extends Publisher { this.deactivate(); } - static createPublisher(nodeHandle, typeClass, topic, options) { + static createPublisher(node, typeClass, topic, options) { let type = typeClass.type(); let handle = rclnodejs.createPublisher( - nodeHandle, + node.handle, type.pkgName, type.subFolder, type.interfaceName, diff --git a/lib/node.js b/lib/node.js index 3cea9152..9b6a1d53 100644 --- a/lib/node.js +++ b/lib/node.js @@ -41,6 +41,8 @@ const TimeSource = require('./time_source.js'); const Timer = require('./timer.js'); const TypeDescriptionService = require('./type_description_service.js'); const Entity = require('./entity.js'); +const { SubscriptionEventCallbacks } = require('../lib/event_handler.js'); +const { PublisherEventCallbacks } = require('../lib/event_handler.js'); // Parameter event publisher constants const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent'; @@ -96,6 +98,7 @@ class Node extends rclnodejs.ShadowNode { this._services = []; this._timers = []; this._guards = []; + this._events = []; this._actionClients = []; this._actionServers = []; this._rateTimerServer = null; @@ -181,6 +184,9 @@ class Node extends rclnodejs.ShadowNode { let actionServersReady = this._actionServers.filter((actionServer) => handles.includes(actionServer.handle) ); + let eventsReady = this._events.filter((event) => + handles.includes(event.handle) + ); timersReady.forEach((timer) => { if (timer.isReady()) { @@ -189,6 +195,10 @@ class Node extends rclnodejs.ShadowNode { } }); + eventsReady.forEach((event) => { + event.takeData(); + }); + for (const subscription of subscriptionsReady) { if (subscription.isDestroyed()) continue; if (subscription.isRaw) { @@ -588,27 +598,39 @@ class Node extends rclnodejs.ShadowNode { * @param {object} options - The options argument used to parameterize the publisher. * @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 publisher, default: QoS.profileDefault. + * @param {PublisherEventCallbacks} eventCallbacks - The event callbacks for the publisher. * @return {Publisher} - An instance of Publisher. */ - createPublisher(typeClass, topic, options) { - return this._createPublisher(typeClass, topic, options, Publisher); + createPublisher(typeClass, topic, options, eventCallbacks) { + return this._createPublisher( + typeClass, + topic, + options, + Publisher, + eventCallbacks + ); } - _createPublisher(typeClass, topic, options, publisherClass) { + _createPublisher(typeClass, topic, options, publisherClass, eventCallbacks) { if (typeof typeClass === 'string' || typeof typeClass === 'object') { typeClass = loader.loadInterface(typeClass); } options = this._validateOptions(options); - if (typeof typeClass !== 'function' || typeof topic !== 'string') { + if ( + typeof typeClass !== 'function' || + typeof topic !== 'string' || + (eventCallbacks && !(eventCallbacks instanceof PublisherEventCallbacks)) + ) { throw new TypeError('Invalid argument'); } let publisher = publisherClass.createPublisher( - this.handle, + this, typeClass, topic, - options + options, + eventCallbacks ); debug('Finish creating publisher, topic = %s.', topic); this._publishers.push(publisher); @@ -643,12 +665,13 @@ class Node extends rclnodejs.ShadowNode { * 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. + * @param {SubscriptionEventCallbacks} eventCallbacks - The event callbacks for the subscription. * @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|Content-filter details at DDS 1.4 specification, Annex B} */ - createSubscription(typeClass, topic, options, callback) { + createSubscription(typeClass, topic, options, callback, eventCallbacks) { if (typeof typeClass === 'string' || typeof typeClass === 'object') { typeClass = loader.loadInterface(typeClass); } @@ -662,17 +685,20 @@ class Node extends rclnodejs.ShadowNode { if ( typeof typeClass !== 'function' || typeof topic !== 'string' || - typeof callback !== 'function' + typeof callback !== 'function' || + (eventCallbacks && + !(eventCallbacks instanceof SubscriptionEventCallbacks)) ) { throw new TypeError('Invalid argument'); } let subscription = Subscription.createSubscription( - this.handle, + this, typeClass, topic, options, - callback + callback, + eventCallbacks ); debug('Finish creating subscription, topic = %s.', topic); this._subscriptions.push(subscription); @@ -836,6 +862,12 @@ class Node extends rclnodejs.ShadowNode { if (!(publisher instanceof Publisher)) { throw new TypeError('Invalid argument'); } + if (publisher.events) { + publisher.events.forEach((event) => { + this._destroyEntity(event, this._events); + }); + publisher.events = []; + } this._destroyEntity(publisher, this._publishers, false); } @@ -848,6 +880,13 @@ class Node extends rclnodejs.ShadowNode { if (!(subscription instanceof Subscription)) { throw new TypeError('Invalid argument'); } + if (subscription.events) { + subscription.events.forEach((event) => { + this._destroyEntity(event, this._events); + }); + subscription.events = []; + } + this._destroyEntity(subscription, this._subscriptions); } diff --git a/lib/publisher.js b/lib/publisher.js index 712339d3..b778b8b7 100644 --- a/lib/publisher.js +++ b/lib/publisher.js @@ -24,8 +24,12 @@ const Entity = require('./entity.js'); */ class Publisher extends Entity { - constructor(handle, typeClass, topic, options) { + constructor(handle, typeClass, topic, options, node, eventCallbacks) { super(handle, typeClass, options); + if (node && eventCallbacks) { + this._events = eventCallbacks.createEventHandlers(this.handle); + node._events.push(...this._events); + } } /** @@ -60,17 +64,24 @@ class Publisher extends Entity { debug(`Message of topic ${this.topic} has been published.`); } - static createPublisher(nodeHandle, typeClass, topic, options) { + static createPublisher(node, typeClass, topic, options, eventCallbacks) { let type = typeClass.type(); let handle = rclnodejs.createPublisher( - nodeHandle, + node.handle, type.pkgName, type.subFolder, type.interfaceName, topic, options.qos ); - return new Publisher(handle, typeClass, topic, options); + return new Publisher( + handle, + typeClass, + topic, + options, + node, + eventCallbacks + ); } /** @@ -99,6 +110,22 @@ class Publisher extends Entity { waitForAllAcked(timeout) { return rclnodejs.waitForAllAcked(this._handle, timeout); } + + /** + * Get the event handlers for this publisher. + * @returns {Array} The array of event handlers for this publisher. + */ + get events() { + return this._events; + } + + /** + * Set the event handlers for this publisher. + * @param {Array} events - The array of event handlers to be set for this publisher. + */ + set events(events) { + this._events = events; + } } module.exports = Publisher; diff --git a/lib/subscription.js b/lib/subscription.js index f563ae01..5f07a58f 100644 --- a/lib/subscription.js +++ b/lib/subscription.js @@ -29,11 +29,24 @@ const debug = require('debug')('rclnodejs:subscription'); */ class Subscription extends Entity { - constructor(handle, typeClass, topic, options, callback) { + constructor( + handle, + typeClass, + topic, + options, + callback, + node, + eventCallbacks + ) { super(handle, typeClass, options); this._topic = topic; this._callback = callback; this._isRaw = options.isRaw || false; + + if (node && eventCallbacks) { + this._events = eventCallbacks.createEventHandlers(this.handle); + node._events.push(...this._events); + } } processResponse(msg) { @@ -45,7 +58,14 @@ class Subscription extends Entity { } } - static createSubscription(nodeHandle, typeClass, topic, options, callback) { + static createSubscription( + node, + typeClass, + topic, + options, + callback, + eventCallbacks + ) { let type = typeClass.type(); // convert contentFilter.parameters to a string[] @@ -56,14 +76,22 @@ class Subscription extends Entity { } let handle = rclnodejs.createSubscription( - nodeHandle, + node.handle, type.pkgName, type.subFolder, type.interfaceName, topic, options ); - return new Subscription(handle, typeClass, topic, options, callback); + return new Subscription( + handle, + typeClass, + topic, + options, + callback, + node, + eventCallbacks + ); } /** @@ -124,6 +152,22 @@ class Subscription extends Entity { get publisherCount() { return rclnodejs.getPublisherCount(this._handle); } + + /** + * Get the event handlers for this subscription. + * @returns {Array} The array of event handlers for this subscription. + */ + get events() { + return this._events; + } + + /** + * Set the event handlers for this subscription. + * @param {Array} events - The array of event handlers for this subscription. + */ + set events(events) { + this._events = events; + } } module.exports = Subscription; diff --git a/src/addon.cpp b/src/addon.cpp index 9d5d93d0..277678a1 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -35,6 +35,7 @@ #include "rcl_time_point_bindings.h" #include "rcl_timer_bindings.h" #if ROS_VERSION > 2205 // ROS2 > Humble +#include "rcl_event_handle_bindings.h" #include "rcl_type_description_service_bindings.h" #endif #include "rcl_utilities.h" @@ -84,6 +85,7 @@ Napi::Object InitModule(Napi::Env env, Napi::Object exports) { rclnodejs::InitTimerBindings(env, exports); #if ROS_VERSION > 2205 // ROS2 > Humble rclnodejs::InitTypeDescriptionServiceBindings(env, exports); + rclnodejs::InitEventHandleBindings(env, exports); #endif rclnodejs::InitLifecycleBindings(env, exports); rclnodejs::ShadowNode::Init(env, exports); diff --git a/src/executor.cpp b/src/executor.cpp index 2f964d7c..633c8bb7 100644 --- a/src/executor.cpp +++ b/src/executor.cpp @@ -190,10 +190,11 @@ RclResult Executor::WaitForReadyCallbacks(rcl_wait_set_t* wait_set, size_t num_timers = 0u; size_t num_clients = 0u; size_t num_services = 0u; + size_t num_events = 0u; rcl_ret_t get_entity_ret = handle_manager_->GetEntityCounts( &num_subscriptions, &num_guard_conditions, &num_timers, &num_clients, - &num_services); + &num_services, &num_events); if (get_entity_ret != RCL_RET_OK) { std::string error_message = std::string("Failed to get entity counts: ") + std::string(rcl_get_error_string().str); @@ -202,9 +203,7 @@ RclResult Executor::WaitForReadyCallbacks(rcl_wait_set_t* wait_set, rcl_ret_t resize_ret = rcl_wait_set_resize(wait_set, num_subscriptions, num_guard_conditions, - num_timers, num_clients, num_services, - // TODO(minggang): support events. - 0u); + num_timers, num_clients, num_services, num_events); if (resize_ret != RCL_RET_OK) { std::string error_message = std::string("Failed to resize: ") + std::string(rcl_get_error_string().str); diff --git a/src/handle_manager.cpp b/src/handle_manager.cpp index ac3e10c8..485198bb 100644 --- a/src/handle_manager.cpp +++ b/src/handle_manager.cpp @@ -40,7 +40,6 @@ HandleManager::~HandleManager() { void HandleManager::SynchronizeHandles(const Napi::Object& node) { Napi::HandleScope scope(node.Env()); - Napi::Value timers = node.Get("_timers"); Napi::Value subscriptions = node.Get("_subscriptions"); Napi::Value clients = node.Get("_clients"); @@ -48,6 +47,7 @@ void HandleManager::SynchronizeHandles(const Napi::Object& node) { Napi::Value guard_conditions = node.Get("_guards"); Napi::Value action_clients = node.Get("_actionClients"); Napi::Value action_servers = node.Get("_actionServers"); + Napi::Value events = node.Get("_events"); uint32_t sum = 0; is_synchronizing_.store(true); @@ -66,6 +66,7 @@ void HandleManager::SynchronizeHandles(const Napi::Object& node) { &action_clients_); sum += SynchronizeHandlesByType(action_servers.As(), &action_servers_); + sum += SynchronizeHandlesByType(events.As(), &events_); } is_synchronizing_.store(false); @@ -98,6 +99,7 @@ void HandleManager::ClearHandles() { guard_conditions_.clear(); action_clients_.clear(); action_servers_.clear(); + events_.clear(); } rcl_ret_t HandleManager::AddHandlesToWaitSet(rcl_wait_set_t* wait_set) { @@ -152,6 +154,11 @@ rcl_ret_t HandleManager::AddHandlesToWaitSet(rcl_wait_set_t* wait_set) { if (ret != RCL_RET_OK) return ret; } + for (auto& event : events_) { + rcl_event_t* rcl_event = reinterpret_cast(event->ptr()); + rcl_ret_t ret = rcl_wait_set_add_event(wait_set, rcl_event, nullptr); + if (ret != RCL_RET_OK) return ret; + } return RCL_RET_OK; } @@ -169,6 +176,8 @@ rcl_ret_t HandleManager::CollectReadyHandles(rcl_wait_set_t* wait_set) { CollectReadyHandlesByType(wait_set->guard_conditions, wait_set->size_of_guard_conditions, guard_conditions_, &ready_handles); + CollectReadyHandlesByType(wait_set->events, wait_set->size_of_events, events_, + &ready_handles); rcl_ret_t ret = CollectReadyActionHandles(wait_set, &ready_handles); if (!ready_handles.empty()) { @@ -184,7 +193,8 @@ rcl_ret_t HandleManager::GetEntityCounts(size_t* subscriptions_size, size_t* guard_conditions_size, size_t* timers_size, size_t* clients_size, - size_t* services_size) { + size_t* services_size, + size_t* events_size) { size_t num_subscriptions = 0u; size_t num_guard_conditions = 0u; size_t num_timers = 0u; @@ -230,6 +240,7 @@ rcl_ret_t HandleManager::GetEntityCounts(size_t* subscriptions_size, *timers_size += timer_count(); *clients_size += client_count(); *services_size += service_count(); + *events_size += event_count(); return RCL_RET_OK; } diff --git a/src/handle_manager.h b/src/handle_manager.h index 0e47a943..70f302ed 100644 --- a/src/handle_manager.h +++ b/src/handle_manager.h @@ -68,13 +68,15 @@ class HandleManager { rcl_ret_t CollectReadyHandles(rcl_wait_set_t* wait_set); rcl_ret_t GetEntityCounts(size_t* subscriptions_size, size_t* guard_conditions_size, size_t* timers_size, - size_t* clients_size, size_t* services_size); + size_t* clients_size, size_t* services_size, + size_t* events_size); uint32_t subscription_count() const { return subscriptions_.size(); } uint32_t service_count() const { return services_.size(); } uint32_t client_count() const { return clients_.size(); } uint32_t timer_count() const { return timers_.size(); } uint32_t guard_condition_count() const { return guard_conditions_.size(); } + uint32_t event_count() const { return events_.size(); } uv_rwlock_t* handle_rwlock() { return &sync_handles_rwlock_; } uint32_t ready_handles_count(); @@ -110,6 +112,7 @@ class HandleManager { std::vector action_servers_; std::vector action_clients_; std::vector ready_handles_; + std::vector events_; // Protects the handles. uv_rwlock_t sync_handles_rwlock_; diff --git a/src/rcl_event_handle_bindings.cpp b/src/rcl_event_handle_bindings.cpp new file mode 100644 index 00000000..46705013 --- /dev/null +++ b/src/rcl_event_handle_bindings.cpp @@ -0,0 +1,294 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// 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. + +#include "rcl_event_handle_bindings.h" + +#include +#include + +#include "rcl_handle.h" + +namespace { + +typedef union event_callback_data { + // Subscription events + rmw_requested_deadline_missed_status_t requested_deadline_missed; + rmw_liveliness_changed_status_t liveliness_changed; + rmw_message_lost_status_t message_lost; + rmw_requested_qos_incompatible_event_status_t requested_incompatible_qos; + rmw_matched_status_t subscription_matched; + // Publisher events + rmw_offered_deadline_missed_status_t offered_deadline_missed; + rmw_liveliness_lost_status_t liveliness_lost; + rmw_offered_qos_incompatible_event_status_t offered_incompatible_qos; + rmw_matched_status_t publisher_matched; + + rmw_incompatible_type_status_t incompatible_type; +} event_callback_data_t; + +rcl_event_t* CreateEventHandle() { + rcl_event_t* event = + reinterpret_cast(malloc(sizeof(rcl_event_t))); + *event = rcl_get_zero_initialized_event(); + return event; +} + +Napi::Value CreateJSObjectForSubscriptionEvent( + Napi::Env env, rcl_subscription_event_type_t subscription_event_type, + const event_callback_data_t& data) { + Napi::Object obj = Napi::Object::New(env); + switch (subscription_event_type) { + case RCL_SUBSCRIPTION_REQUESTED_DEADLINE_MISSED: { + obj.Set( + "total_count", + Napi::Number::New(env, data.requested_deadline_missed.total_count)); + obj.Set("total_count_change", + Napi::Number::New( + env, data.requested_deadline_missed.total_count_change)); + break; + } + case RCL_SUBSCRIPTION_LIVELINESS_CHANGED: { + obj.Set("alive_count", + Napi::Number::New(env, data.liveliness_changed.alive_count)); + obj.Set("not_alive_count", + Napi::Number::New(env, data.liveliness_changed.not_alive_count)); + obj.Set( + "alive_count_change", + Napi::Number::New(env, data.liveliness_changed.alive_count_change)); + obj.Set("not_alive_count_change", + Napi::Number::New( + env, data.liveliness_changed.not_alive_count_change)); + break; + } + case RCL_SUBSCRIPTION_MESSAGE_LOST: { + obj.Set("total_count", + Napi::Number::New(env, data.message_lost.total_count)); + obj.Set("total_count_change", + Napi::Number::New(env, data.message_lost.total_count_change)); + break; + } + case RCL_SUBSCRIPTION_REQUESTED_INCOMPATIBLE_QOS: { + obj.Set( + "total_count", + Napi::Number::New(env, data.requested_incompatible_qos.total_count)); + obj.Set("total_count_change", + Napi::Number::New( + env, data.requested_incompatible_qos.total_count_change)); + obj.Set("last_policy_kind", + Napi::Number::New( + env, data.requested_incompatible_qos.last_policy_kind)); + break; + } + case RCL_SUBSCRIPTION_INCOMPATIBLE_TYPE: { + obj.Set("total_count", + Napi::Number::New(env, data.incompatible_type.total_count)); + obj.Set( + "total_count_change", + Napi::Number::New(env, data.incompatible_type.total_count_change)); + break; + } + case RCL_SUBSCRIPTION_MATCHED: { + obj.Set("total_count", + Napi::Number::New(env, data.subscription_matched.total_count)); + obj.Set( + "total_count_change", + Napi::Number::New(env, data.subscription_matched.total_count_change)); + obj.Set("current_count", + Napi::Number::New(env, data.subscription_matched.current_count)); + obj.Set("current_count_change", + Napi::Number::New( + env, data.subscription_matched.current_count_change)); + break; + } + default: + break; + } + return obj; +} + +Napi::Value CreateJSObjectForPublisherEvent( + Napi::Env env, rcl_publisher_event_type_t publisher_event_type, + const event_callback_data_t& data) { + Napi::Object obj = Napi::Object::New(env); + switch (publisher_event_type) { + case RCL_PUBLISHER_OFFERED_DEADLINE_MISSED: { + obj.Set("total_count", + Napi::Number::New(env, data.offered_deadline_missed.total_count)); + obj.Set("total_count_change", + Napi::Number::New( + env, data.offered_deadline_missed.total_count_change)); + break; + } + case RCL_PUBLISHER_LIVELINESS_LOST: { + obj.Set("total_count", + Napi::Number::New(env, data.liveliness_lost.total_count)); + obj.Set("total_count_change", + Napi::Number::New(env, data.liveliness_lost.total_count_change)); + break; + } + case RCL_PUBLISHER_OFFERED_INCOMPATIBLE_QOS: { + obj.Set( + "total_count", + Napi::Number::New(env, data.offered_incompatible_qos.total_count)); + obj.Set("total_count_change", + Napi::Number::New( + env, data.offered_incompatible_qos.total_count_change)); + obj.Set("last_policy_kind", + Napi::Number::New( + env, data.offered_incompatible_qos.last_policy_kind)); + break; + } + case RCL_PUBLISHER_INCOMPATIBLE_TYPE: { + obj.Set("total_count", + Napi::Number::New(env, data.incompatible_type.total_count)); + obj.Set( + "total_count_change", + Napi::Number::New(env, data.incompatible_type.total_count_change)); + break; + } + case RCL_PUBLISHER_MATCHED: { + obj.Set("total_count", + Napi::Number::New(env, data.publisher_matched.total_count)); + obj.Set( + "total_count_change", + Napi::Number::New(env, data.publisher_matched.total_count_change)); + obj.Set("current_count", + Napi::Number::New(env, data.publisher_matched.current_count)); + obj.Set( + "current_count_change", + Napi::Number::New(env, data.publisher_matched.current_count_change)); + break; + } + default: + break; + } + return obj; +} + +} // namespace + +namespace rclnodejs { + +Napi::Value CreateSubscriptionEventHandle(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()); + rcl_subscription_event_type_t event_type = + static_cast( + info[1].As().Int32Value()); + + rcl_event_t* event = CreateEventHandle(); + rcl_ret_t ret = rcl_subscription_event_init(event, subscription, event_type); + + if (ret != RCL_RET_OK) { + Napi::Error::New(env, "failed to create subscription event") + .ThrowAsJavaScriptException(); + rcl_reset_error(); + free(event); + return env.Undefined(); + } + + auto js_obj = RclHandle::NewInstance(env, event, nullptr, [env](void* ptr) { + rcl_event_t* event = reinterpret_cast(ptr); + rcl_ret_t ret = rcl_event_fini(event); + if (ret != RCL_RET_OK) { + Napi::Error::New(env, rcl_get_error_string().str) + .ThrowAsJavaScriptException(); + rcl_reset_error(); + } + free(ptr); + }); + return js_obj; +} + +Napi::Value CreatePublisherEventHandle(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + RclHandle* publisher_handle = RclHandle::Unwrap(info[0].As()); + rcl_publisher_t* publisher = + reinterpret_cast(publisher_handle->ptr()); + rcl_publisher_event_type_t event_type = + static_cast( + info[1].As().Int32Value()); + + rcl_event_t* event = CreateEventHandle(); + rcl_ret_t ret = rcl_publisher_event_init(event, publisher, event_type); + + if (ret != RCL_RET_OK) { + Napi::Error::New(env, "failed to create publisher event") + .ThrowAsJavaScriptException(); + rcl_reset_error(); + free(event); + return env.Undefined(); + } + + auto js_obj = RclHandle::NewInstance(env, event, nullptr, [env](void* ptr) { + rcl_event_t* event = reinterpret_cast(ptr); + rcl_ret_t ret = rcl_event_fini(event); + if (ret != RCL_RET_OK) { + Napi::Error::New(env, rcl_get_error_string().str) + .ThrowAsJavaScriptException(); + rcl_reset_error(); + } + free(ptr); + }); + return js_obj; +} + +Napi::Value TakeEvent(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + RclHandle* event_handle = RclHandle::Unwrap(info[0].As()); + rcl_event_t* event = reinterpret_cast(event_handle->ptr()); + auto event_type = info[1].As(); + + event_callback_data_t data; + rcl_ret_t ret; + if (event_type.Has("subscription_event_type")) { + rcl_subscription_event_type_t subscription_event_type = + static_cast( + event_type.Get("subscription_event_type") + .As() + .Int32Value()); + ret = rcl_take_event(event, &data); + if (RCL_RET_OK == ret) { + return CreateJSObjectForSubscriptionEvent(env, subscription_event_type, + data); + } + } else if (event_type.Has("publisher_event_type")) { + rcl_publisher_event_type_t publisher_event_type = + static_cast( + event_type.Get("publisher_event_type") + .As() + .Int32Value()); + ret = rcl_take_event(event, &data); + if (RCL_RET_OK == ret) { + return CreateJSObjectForPublisherEvent(env, publisher_event_type, data); + } + } + + Napi::Error::New(env, "failed to take event").ThrowAsJavaScriptException(); + return env.Undefined(); +} + +Napi::Object InitEventHandleBindings(Napi::Env env, Napi::Object exports) { + exports.Set("createSubscriptionEventHandle", + Napi::Function::New(env, CreateSubscriptionEventHandle)); + exports.Set("createPublisherEventHandle", + Napi::Function::New(env, CreatePublisherEventHandle)); + exports.Set("takeEvent", Napi::Function::New(env, TakeEvent)); + return exports; +} + +} // namespace rclnodejs diff --git a/src/rcl_event_handle_bindings.h b/src/rcl_event_handle_bindings.h new file mode 100644 index 00000000..98a41c02 --- /dev/null +++ b/src/rcl_event_handle_bindings.h @@ -0,0 +1,26 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// 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. + +#ifndef SRC_RCL_EVENT_HANDLE_BINDINGS_H_ +#define SRC_RCL_EVENT_HANDLE_BINDINGS_H_ + +#include + +namespace rclnodejs { + +Napi::Object InitEventHandleBindings(Napi::Env env, Napi::Object exports); + +} + +#endif // SRC_RCL_EVENT_HANDLE_BINDINGS_H_ diff --git a/test/test-event-handle.js b/test/test-event-handle.js new file mode 100644 index 00000000..36c8fbaf --- /dev/null +++ b/test/test-event-handle.js @@ -0,0 +1,241 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// 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 assert = require('assert'); +const DistroUtils = require('../lib/distro.js'); +const rclnodejs = require('../index.js'); +const { SubscriptionEventCallbacks } = require('../lib/event_handler.js'); +const { PublisherEventCallbacks } = require('../lib/event_handler.js'); + +describe('Event handle test suite prior to jazzy', function () { + before(function () { + if (DistroUtils.getDistroId() >= DistroUtils.getDistroId('jazzy')) { + this.skip(); + } + }); + + it('Error expected when creating SubscriptionEventCallbacks', function () { + assert.throws(() => { + new SubscriptionEventCallbacks(); + }, /SubscriptionEventCallbacks is only available in ROS 2 Jazzy and later.$/); + }); + + it('Error expected when creating PublisherEventCallbacks', function () { + assert.throws(() => { + new PublisherEventCallbacks(); + }, /PublisherEventCallbacks is only available in ROS 2 Jazzy and later.$/); + }); +}); + +describe('Event handle test suite', function () { + this.timeout(5 * 1000); + let node; + + before(function () { + if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) { + this.skip(); + } + }); + + beforeEach(async function () { + await rclnodejs.init(); + const nodeName = 'test_event_handle'; + node = rclnodejs.createNode(nodeName); + rclnodejs.spin(node); + }); + + afterEach(function () { + rclnodejs.shutdown(); + }); + + it('Test subscription constructor with callback', function () { + const String = 'std_msgs/msg/String'; + let eventCallbacks = new SubscriptionEventCallbacks(); + let subscription = null; + let expectedEventCount = 1; + eventCallbacks.deadline = () => {}; + subscription = node.createSubscription( + String, + 'topic', + undefined, + (msg) => {}, + eventCallbacks + ); + assert.strictEqual(node._events.length, expectedEventCount); + node.destroySubscription(subscription); + + eventCallbacks = new SubscriptionEventCallbacks(); + eventCallbacks.liveliness = () => {}; + eventCallbacks.deadline = () => {}; + expectedEventCount++; + node.createSubscription( + String, + 'topic', + undefined, + (msg) => {}, + eventCallbacks + ); + assert.strictEqual(node._events.length, expectedEventCount); + }); + + it('Test subscription event apis', async function () { + const String = 'std_msgs/msg/String'; + let eventCallbacks = new SubscriptionEventCallbacks(); + const deadlinePromise = new Promise((resolve) => { + eventCallbacks.deadline = (deadline) => { + assert.strictEqual(deadline.total_count, 0); + assert.strictEqual(deadline.total_count_change, 0); + resolve(); + }; + }); + const livelinessPromise = new Promise((resolve) => { + eventCallbacks.liveliness = (liveliness) => { + assert.strictEqual(liveliness.alive_count, 0); + assert.strictEqual(liveliness.not_alive_count, 0); + assert.strictEqual(liveliness.alive_count_change, 0); + assert.strictEqual(liveliness.not_alive_count_change, 0); + resolve(); + }; + }); + const incompatibleQosPromise = new Promise((resolve) => { + eventCallbacks.incompatibleQos = (incompatibleQos) => { + assert.strictEqual(incompatibleQos.total_count, 0); + assert.strictEqual(incompatibleQos.total_count_change, 0); + resolve(); + }; + }); + + node.createSubscription( + String, + 'topic', + undefined, + (msg) => {}, + eventCallbacks + ); + node._events.forEach((event) => { + event.takeData(); + }); + + return Promise.all([ + deadlinePromise, + livelinessPromise, + incompatibleQosPromise, + ]); + }); + + it('Test publisher event apis', async function () { + const String = 'std_msgs/msg/String'; + let eventCallbacks = new PublisherEventCallbacks(); + const deadlinePromise = new Promise((resolve) => { + eventCallbacks.deadline = (deadline) => { + assert.strictEqual(deadline.total_count, 0); + assert.strictEqual(deadline.total_count_change, 0); + resolve(); + }; + }); + const livelinessPromise = new Promise((resolve) => { + eventCallbacks.liveliness = (liveliness) => { + assert.strictEqual(liveliness.total_count, 0); + assert.strictEqual(liveliness.total_count_change, 0); + resolve(); + }; + }); + const incompatibleQosPromise = new Promise((resolve) => { + eventCallbacks.incompatibleQos = (incompatibleQos) => { + assert.strictEqual(incompatibleQos.total_count, 0); + assert.strictEqual(incompatibleQos.total_count_change, 0); + resolve(); + }; + }); + + node.createPublisher(String, 'topic', undefined, eventCallbacks); + node._events.forEach((event) => { + event.takeData(); + }); + + return Promise.all([ + deadlinePromise, + livelinessPromise, + incompatibleQosPromise, + ]); + }); + + it('Test subscription event matched', function (done) { + const String = 'std_msgs/msg/String'; + let publisher = null; + let publisherDestroyed = false; + let eventCallbacks = new SubscriptionEventCallbacks(); + eventCallbacks.matched = (matched) => { + if (!publisherDestroyed) { + assert.strictEqual(matched.total_count, 1); + assert.strictEqual(matched.total_count_change, 1); + assert.strictEqual(matched.current_count, 1); + assert.strictEqual(matched.current_count_change, 1); + node.destroyPublisher(publisher); + publisherDestroyed = true; + } else { + assert.strictEqual(matched.total_count, 1); + assert.strictEqual(matched.total_count_change, 0); + assert.strictEqual(matched.current_count, 0); + assert.strictEqual(matched.current_count_change, -1); + node.stop(); + done(); + } + }; + node.createSubscription( + String, + 'topic', + undefined, + (msg) => {}, + eventCallbacks + ); + assert.strictEqual(node._events.length, 1); + + publisher = node.createPublisher(String, 'topic', undefined); + }); + + it('Test publisher event unmatched', function (done) { + const String = 'std_msgs/msg/String'; + let subscription = null; + let subscriptionDestroyed = false; + let eventCallbacks = new PublisherEventCallbacks(); + eventCallbacks.matched = (matched) => { + if (!subscriptionDestroyed) { + assert.strictEqual(matched.total_count, 1); + assert.strictEqual(matched.total_count_change, 1); + assert.strictEqual(matched.current_count, 1); + assert.strictEqual(matched.current_count_change, 1); + node.destroySubscription(subscription); + subscriptionDestroyed = true; + } else { + assert.strictEqual(matched.total_count, 1); + assert.strictEqual(matched.total_count_change, 0); + assert.strictEqual(matched.current_count, 0); + assert.strictEqual(matched.current_count_change, -1); + node.stop(); + done(); + } + }; + + node.createPublisher(String, 'topic', undefined, eventCallbacks); + subscription = node.createSubscription( + String, + 'topic', + undefined, + (msg) => {} + ); + }); +}); diff --git a/test/test-type-description-service.js b/test/test-type-description-service.js index 8263bfa7..58813157 100644 --- a/test/test-type-description-service.js +++ b/test/test-type-description-service.js @@ -15,6 +15,7 @@ 'use strict'; const assert = require('assert'); +const assertUtils = require('./utils.js'); const DistroUtils = require('../lib/distro.js'); const rclnodejs = require('../index.js'); const TypeDescriptionService = require('../lib/type_description_service.js'); @@ -23,11 +24,18 @@ describe('type description service test suite', function () { this.timeout(60 * 1000); let node; + before(function () { + if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) { + this.skip(); + } + }); + beforeEach(async function () { await rclnodejs.init(); const nodeName = 'test_type_description_service'; node = rclnodejs.createNode(nodeName); rclnodejs.spin(node); + await assertUtils.createDelay(1000); }); afterEach(function () { @@ -35,10 +43,6 @@ describe('type description service test suite', function () { }); it('Test type description service', function (done) { - if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) { - this.skip(); - return; - } // Create a publisher const topic = 'test_get_type_description_publisher'; const topicType = 'std_msgs/msg/String'; diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index fb1fb8df..33cd0caa 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -140,6 +140,9 @@ expectType(publisher.publish(Buffer.from('Hello ROS World'))); expectType(node.destroyPublisher(publisher)); expectType(publisher.isDestroyed()); expectType(publisher.waitForAllAcked(BigInt(1000))); +node.createPublisher(TYPE_CLASS, TOPIC, publisher.options, (event: object) => { + const receivedEvent = event; +}); // ---- LifecyclePublisher ---- const lifecyclePublisher = lifecycleNode.createLifecyclePublisher( @@ -155,7 +158,15 @@ expectType(lifecyclePublisher.isActivated()); let subscription = node.createSubscription(TYPE_CLASS, TOPIC, () => {}); expectType(subscription); expectType( - node.createSubscription(TYPE_CLASS, TOPIC, {}, () => {}) + node.createSubscription( + TYPE_CLASS, + TOPIC, + {}, + () => {}, + (event: object) => { + const receivedEvent = event; + } + ) ); const contentFilter: rclnodejs.SubscriptionContentFilter = { diff --git a/types/node.d.ts b/types/node.d.ts index 04a79ce2..4146be07 100644 --- a/types/node.d.ts +++ b/types/node.d.ts @@ -303,12 +303,14 @@ declare module 'rclnodejs' { * @param typeClass - Type of message that will be published. * @param topic - Name of the topic the publisher will publish to. * @param options - Configuration options, see DEFAULT_OPTIONS + * @param eventCallbacks - Optional The event callbacks for the publisher. * @returns New instance of Publisher. */ createPublisher>( typeClass: T, topic: string, - options?: Options + options?: Options, + eventCallbacks?: (event: object) => void ): Publisher; /** @@ -333,6 +335,7 @@ declare module 'rclnodejs' { * @param topic - Name of the topic the subcription will subscribe to. * @param options - Configuration options, see DEFAULT_OPTIONS * @param callback - Called when a new message is received. The serialized message will be null-terminated. + * @param eventCallbacks - Optional The event callbacks for the subscription. * @returns New instance of Subscription. * @throws Error - May throw an RMW error if options.content-filter is malformed. * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B} @@ -341,7 +344,8 @@ declare module 'rclnodejs' { typeClass: T, topic: string, options: Options, - callback: SubscriptionCallback | SubscriptionWithRawMessageCallback + callback: SubscriptionCallback | SubscriptionWithRawMessageCallback, + eventCallbacks?: (event: object) => void ): Subscription; /**