diff --git a/binding.gyp b/binding.gyp index 6743563b..54f69ec7 100644 --- a/binding.gyp +++ b/binding.gyp @@ -20,6 +20,7 @@ './src/rcl_action_bindings.cpp', './src/rcl_bindings.cpp', './src/rcl_handle.cpp', + './src/rcl_lifecycle_bindings.cpp', './src/rcl_utilities.cpp', './src/shadow_node.cpp', ], @@ -42,6 +43,7 @@ 'libraries': [ '-lrcl', '-lrcl_action', + '-lrcl_lifecycle', '-lrcutils', '-lrcl_yaml_param_parser', '-lrmw', diff --git a/example/lifecycle-node-example.js b/example/lifecycle-node-example.js new file mode 100644 index 00000000..eb3f05fe --- /dev/null +++ b/example/lifecycle-node-example.js @@ -0,0 +1,121 @@ +// Copyright (c) 2020 Wayne Parrott. All rights reserved. +// +// 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('../index.js'); + +const NODE_NAME = 'test_node'; +const TOPIC = 'test'; +const COUNTD_DOWN = 5; + +/** + * This app demonstrates using a LifecycleNode to + * publish a count down value from 10 - 0. A subscription + * is created to watch for the counter reaching 0 at which + * time it will deactivate and shutdown the node. + */ +class App { + + constructor() { + this._node = null; + this._publisher = null; + this._subscriber = null; + this._timer = null; + this._StateInterface = null; + } + + async init() { + await rclnodejs.init(); + + this._count = COUNTD_DOWN; + this._node = rclnodejs.createLifecycleNode(NODE_NAME); + this._node.registerOnConfigure((prevState)=>this.onConfigure(prevState)); + this._node.registerOnActivate((prevState)=>this.onActivate(prevState)); + this._node.registerOnDeactivate((prevState)=>this.onDeactivate(prevState)); + this._node.registerOnShutdown((prevState)=>this.onShutdown(prevState)); + this._StateInterface = rclnodejs.createMessage('lifecycle_msgs/msg/State').constructor; + + rclnodejs.spin(this._node); + } + + start() { + this._node.configure(); + this._node.activate(); + } + + stop() { + this._node.deactivate(); + this._node.shutdown(); + rclnodejs.shutdown(); + process.exit(0); + } + + onConfigure() { + console.log('Lifecycle: CONFIGURE'); + this._publisher = + this._node.createLifecyclePublisher('std_msgs/msg/String', TOPIC); + this._subscriber = + this._node.createSubscription('std_msgs/msg/String', TOPIC, + (msg) => { + let cnt = parseInt(msg.data, 10); + console.log(`countdown msg: ${cnt}`); + if (cnt < 1) { + this.stop(); + } + }); + return rclnodejs.lifecycle.CallbackReturnCode.SUCCESS; + } + + onActivate() { + console.log('Lifecycle: ACTIVATE'); + this._publisher.activate(); + this._timer = this._node.createTimer(1000, () => { + this._publisher.publish(`${this._count--}`); + }); + return rclnodejs.lifecycle.CallbackReturnCode.SUCCESS; + } + + onDeactivate() { + console.log('Lifecycle: DEACTIVATE'); + this._publisher.deactivate(); + if (this._timer) { + this._timer.cancel(); + this._timer = null; + } + return rclnodejs.lifecycle.CallbackReturnCode.SUCCESS; + } + + onShutdown(prevState) { + console.log('Lifecycle: SHUTDOWN'); + let result = rclnodejs.lifecycle.CallbackReturnCode.SUCCESS; + if (prevState.id === this._StateInterface.PRIMARY_STATE) { + result = this.onDeactivate(); + this._publisher = null; + this._subscriber = null; + } + + return result; + } +} + +async function main() { + let app = new App(); + await app.init(); + app.start(); +} + +main(); + + diff --git a/index.js b/index.js index d63d42db..0a4e567a 100644 --- a/index.js +++ b/index.js @@ -49,6 +49,7 @@ const { getActionServerNamesAndTypesByNode, getActionNamesAndTypes, } = require('./lib/action/graph.js'); +const Lifecycle = require('./lib/lifecycle.js'); function inherits(target, source) { const properties = Object.getOwnPropertyNames(source.prototype); @@ -123,6 +124,9 @@ let rcl = { /** {@link IntegerRange} class */ IntegerRange: IntegerRange, + /** Lifecycle namespace */ + lifecycle: Lifecycle, + /** {@link Logging} class */ logging: logging, @@ -192,33 +196,26 @@ let rcl = { context = Context.defaultContext(), options = NodeOptions.defaultOptions ) { - if (typeof nodeName !== 'string' || typeof namespace !== 'string') { - throw new TypeError('Invalid argument.'); - } - - if (!this._contextToNodeArrayMap.has(context)) { - throw new Error( - 'Invalid context. Must call rclnodejs(context) before using the context' - ); - } + return _createNode(nodeName, namespace, context, options, rclnodejs.ShadowNode); + }, - const handle = rclnodejs.createNode(nodeName, namespace, context.handle); - const node = new rclnodejs.ShadowNode(); - node.handle = handle; - Object.defineProperty(node, 'handle', { - configurable: false, - writable: false, - }); // make read-only - node.context = context; - node.init(nodeName, namespace, context, options); - debug( - 'Finish initializing node, name = %s and namespace = %s.', - nodeName, - namespace - ); - - this._contextToNodeArrayMap.get(context).push(node); - return node; + /** + * Create a managed Node that implements a well-defined life-cycle state + * model using the {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}. + * @param {string} nodeName - The name used to register in ROS. + * @param {string} [namespace=''] - The namespace used in ROS. + * @param {Context} [context=Context.defaultContext()] - The context to create the node in. + * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior. + * @return {LifecycleNode} A new instance of the specified node. + * @throws {Error} If the given context is not registered. + */ + createLifecycleNode( + nodeName, + namespace = '', + context = Context.defaultContext(), + options = NodeOptions.defaultOptions + ) { + return _createNode(nodeName, namespace, context, options, Lifecycle.LifecycleNode); }, /** @@ -442,3 +439,30 @@ const TimeSource = require('./lib/time_source.js'); rcl.TimeSource = TimeSource; inherits(rclnodejs.ShadowNode, Node); + +function _createNode( + nodeName, + namespace = '', + context = Context.defaultContext(), + options = NodeOptions.defaultOptions, + nodeClass +) { + if (typeof nodeName !== 'string' || typeof namespace !== 'string') { + throw new TypeError('Invalid argument.'); + } + + if (!rcl._contextToNodeArrayMap.has(context)) { + throw new Error('Invalid context. Must call rclnodejs(context) before using the context'); + } + + let handle = rclnodejs.createNode(nodeName, namespace, context.handle); + let node = new nodeClass(); + node.handle = handle; + Object.defineProperty(node, 'handle', { configurable: false, writable: false }); // make read-only + node.context = context; + node.init(nodeName, namespace, context, options); + debug('Finish initializing node, name = %s and namespace = %s.', nodeName, namespace); + + rcl._contextToNodeArrayMap.get(context).push(node); + return node; +} diff --git a/lib/lifecycle.js b/lib/lifecycle.js new file mode 100644 index 00000000..9ed97215 --- /dev/null +++ b/lib/lifecycle.js @@ -0,0 +1,697 @@ +// Copyright (c) 2020 Wayne Parrott. All rights reserved. +// +// 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 LifecyclePublisher = require('./lifecycle_publisher.js'); +const loader = require('./interface_loader.js'); +const Service = require('./service.js'); + +const SHUTDOWN_TRANSITION_LABEL = rclnodejs.getLifecycleShutdownTransitionLabel(); + +// An instance of State message constructor used for accessing State +// state machine constants. This interface is lazy initialized at runtime. +let StateInterface; + +// An instance of Transition message constructor used for accessing Transition +// state machine constants. This interface is lazy initialized at runtime. +let TransitionInterface; + +function getStateInterface() { + if (!StateInterface) { + StateInterface = loader.loadInterface('lifecycle_msgs/msg/State'); + } + return StateInterface; +} + +function getTransitionInterface() { + if (!TransitionInterface) { + TransitionInterface = loader.loadInterface('lifecycle_msgs/msg/Transition'); + } + return TransitionInterface; +} + +/** + * @typedef SerializedState + * @type {object} + * @property {number} id - code identifying the type of this state. + * @property {string} label - readable name of this state. + */ + +/** + * The state of the lifecycle state model. + */ +class State { + + /** + * Create a state. + * @param {number} id - The id value. + * @param {string} label - The label value. + */ + constructor(id, label) { + this.id = id; + this.label = label; + } + + /** + * Create a State from a SerializedState + * @param {SerializedState} aSerializedState - The state object. + * @returns {State} The State converted from a SerializdState + * @private + */ + static fromSerializedState(aSerializedState) { + return new State(aSerializedState.id, aSerializedState.label); + } +} + +/** + * The intermediate state of the lifecycle state model during a state + * transition. + */ +class Transition extends State { +} + +/** + * Describes a state transition. + */ +class TransitionDescription { + + /** + * Create a transition description. + * + * @param {Transition} transition - The transition + * @param {State} startState - The initial + * @param {State} goalState - The target state of a transition activity + */ + constructor(transition, startState, goalState) { + this.transition = transition; + this.startState = startState; + this.goalState = goalState; + } +} + +/** + * The values returned by TransitionCallback. + * @readonly + * @enum {number} + */ +const CallbackReturnCode = { + get SUCCESS() { return getTransitionInterface().TRANSITION_CALLBACK_SUCCESS; }, + get FAILURE() { return getTransitionInterface().TRANSITION_CALLBACK_FAILURE; }, + get ERROR() { return getTransitionInterface().TRANSITION_CALLBACK_ERROR; } +}; +Object.freeze(CallbackReturnCode); + +/** + * A ValueHolder for a CallbackReturnCode. + */ +class CallbackReturnValue { + + /** + * Creates a new instance. + * + * @param {number} [value=CallbackReturnCode.SUCCESS] - The value property. + */ + constructor(value = CallbackReturnCode.SUCCESS) { + this._value = value; + this._errorMsg = null; + } + + /** + * Access the callbackReturnCode. + * @returns {number} The CallbackReturnCode. + */ + get value() { + return this._value; + } + + set value(value) { + this._value = value; + } + + /** + * Access an optional error message when value is not SUCCESS. + */ + get errorMsg() { + return this._errorMsg; + } + + /** + * Assign the error message. + * @param {string} msg - The error message. + * @returns {unknown} void. + */ + set errorMsg(msg) { + this._errorMsg = msg; + } + + /** + * Overrides Object.valueOf() to return the 'value' property. + * @returns {number} The property value. + */ + valueOf() { + return this.value; + } + + /** + * A predicate to test if the value is SUCCESS. + * @returns {boolean} Return true if the value is SUCCESS; otherwise return false. + */ + isSuccess() { + return this.value === CallbackReturnCode.SUCCESS; + } + + /** + * A predicate to test if the value is FAILURE. + * @returns {boolean} Return true if the value is FAILURE; otherwise return false. + */ + isFailure() { + return this.value === CallbackReturnCode.FAILURE; + } + + /** + * A predicate to test if the value is ERROR. + * @returns {boolean} Return true if the value is ERROR; otherwise return false. + */ + isError() { + return this.value === CallbackReturnCode.ERROR; + } + + /** + * A predicate to test if an error message has been assigned. + * @returns {boolean} Return true if an error message has been assigned; otherwise return false. + */ + hasErrorMsg() { + return !this.isSuccess() && this._errorMsg; + } +} + +/** + * This callback is invoked when LifecycleNode is transitioning to a new state. + * @callback TransitionCallback + * @param {State} previousState - The previous node lifecycle state. + * @return {CallbackReturnCode} - The result of the callback. + * + * @see [LifecycleNode.registerOnConfigure]{@link LifecycleNode#registerOnConfigure} + * @see [LifecycleNode.registerOnCleanup]{@link LifecycleNode#registerOnCleanup} + * @see [LifecycleNode.registerOnActivate]{@link LifecycleNode#registerOnActivate} + * @see [LifecycleNode.registerOnDeactivate]{@link LifecycleNode#registerOnDeactivate} + * @see [LifecycleNode.registerOnShutdown]{@link LifecycleNode#registerOnShutdown} + * @see [LifecycleNode.registerOnError]{@link LifecycleNode#registerOnError} + */ + +/** + * A ROS2 managed Node that implements a well-defined life-cycle state model using the + * {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}. + * @extends Node + * + * This class implments the ROS2 life-cycle state-machine defined by the + * {@link https://github.com/ros2/rclcpp/tree/master/rclcpp_lifecycle}|ROS2 Managed Nodes Design} + * and parallels the {@link https://github.com/ros2/rclcpp/tree/master/rclcpp_lifecycle|rclcpp lifecycle node } + * implementation. + * + * The design consists of four primary lifecycle states: + * UNCONFIGURED + * INACTIVE + * ACTIVE + * FINALIZED. + * + * Transitioning between states is accomplished using an action api: + * configure() + * activate() + * deactivate(), + * cleanup() + * shutdown() + * + * During a state transition, the state-machine is in one of the + * intermediate transitioning states: + * CONFIGURING + * ACTIVATING + * DEACTIVATING + * CLEANINGUP + * SHUTTING_DOWN + * ERROR_PROCESSING + * + * Messaging: + * State changes are published on the '/transition_event' topic. + * Lifecycle service interfaces are also implemented. + * + * You can introduce your own state specific behaviors in the form of a + * {@link TransitionCallback} functions that you register using: + * registerOnConfigure(cb) + * registerOnActivate(cb) + * registerOnDeactivate(cb) + * registerOnCleanup(cb) + * registerOnShutdown(cb) + * registerOnError(cb) + * + * @hideconstructor + */ + +class LifecycleNode extends rclnodejs.ShadowNode { + + init(name, namespace, context, options) { + super.init(name, namespace, context, options); + + // initialize native handle to rcl_lifecycle_state_machine + this._stateMachineHandle = rclnodejs.createLifecycleStateMachine(this.handle); + + // initialize Map + this._callbackMap = new Map(); + + // Setup and register the 4 native rcl lifecycle services thar are + // part of _stateMachineHandle. + let srvHandleObj = + rclnodejs.getLifecycleSrvNameAndHandle( + this.handle, + this._stateMachineHandle, + 'srv_get_state'); + let service = new Service( + srvHandleObj.handle, + srvHandleObj.name, + loader.loadInterface('lifecycle_msgs/srv/GetState'), + this._validateOptions(undefined), + (request, response) => this._onGetState(request, response) + ); + this._services.push(service); + + srvHandleObj = + rclnodejs.getLifecycleSrvNameAndHandle( + this.handle, + this._stateMachineHandle, + 'srv_get_available_states'); + service = new Service( + srvHandleObj.handle, + srvHandleObj.name, + loader.loadInterface('lifecycle_msgs/srv/GetAvailableStates'), + this._validateOptions(undefined), + (request, response) => this._onGetAvailableStates(request, response) + ); + this._services.push(service); + + srvHandleObj = + rclnodejs.getLifecycleSrvNameAndHandle( + this.handle, + this._stateMachineHandle, + 'srv_get_available_transitions'); + service = new Service( + srvHandleObj.handle, + srvHandleObj.name, + loader.loadInterface('lifecycle_msgs/srv/GetAvailableTransitions'), + this._validateOptions(undefined), + (request, response) => this._onGetAvailableTransitions(request, response) + ); + this._services.push(service); + + srvHandleObj = + rclnodejs.getLifecycleSrvNameAndHandle( + this.handle, + this._stateMachineHandle, + 'srv_change_state'); + service = new Service( + srvHandleObj.handle, + srvHandleObj.name, + loader.loadInterface('lifecycle_msgs/srv/ChangeState'), + this._validateOptions(undefined), + (request, response) => this._onChangeState(request, response) + ); + this._services.push(service); + + this.syncHandles(); + } + + /** + * Create a LifecyclePublisher. + * @param {function|string|object} typeClass - The ROS message class, + OR a string representing the message class, e.g. 'std_msgs/msg/String', + OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'} + * @param {string} topic - The name of the topic. + * @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. + * @return {LifecyclePublisher} - An instance of LifecyclePublisher. + */ + createLifecyclePublisher(typeClass, topic, options) { + return this._createPublisher(typeClass, topic, options, LifecyclePublisher); + } + + /** + * Access the current lifecycle state. + * @returns {State} The current state. + */ + get currentState() { + let currentStateObj = + rclnodejs.getCurrentLifecycleState(this._stateMachineHandle); + return State.fromSerializedState(currentStateObj); + } + + /** + * Retrieve all registered states of the state-machine. + * @returns {State[]} The states. + */ + get availableStates() { + let stateObjs = rclnodejs.getLifecycleStates(this._stateMachineHandle); + let states = stateObjs.map((stateObj) => new State(stateObj.id, stateObj.label)); + return states; + } + + /** + * Retrieve all registered transitions of the state-machine. + * + * @returns {TransitionDescription[]} The registered TransitionDescriptions. + */ + get transitions() { + let transitionObjs = rclnodejs.getLifecycleTransitions(this._stateMachineHandle); + let transitions = transitionObjs.map( + (transitionDescObj) => { + let transition = new Transition(transitionDescObj.transition.id, transitionDescObj.transition.label); + let startState = new State(transitionDescObj.start_state.id, transitionDescObj.start_state.label); + let goalState = new State(transitionDescObj.goal_state.id, transitionDescObj.goal_state.label); + return new TransitionDescription(transition, startState, goalState); + }); + return transitions; + } + + /** + * Retrieve the valid transitions available from the current state of the + * state-machine. + * + * @returns {TransitionDescription[]} The available TransitionDescriptions. + */ + get availableTransitions() { + let transitionObjs = rclnodejs.getAvailableLifecycleTransitions(this._stateMachineHandle); + let transitions = transitionObjs.map( + (transitionDescObj) => { + let transition = new Transition(transitionDescObj.transition.id, transitionDescObj.transition.label); + let startState = new State(transitionDescObj.start_state.id, transitionDescObj.start_state.label); + let goalState = new State(transitionDescObj.goal_state.id, transitionDescObj.goal_state.label); + return new TransitionDescription(transition, startState, goalState); + }); + return transitions; + } + + /** + * Register a callback function to be invoked during the configure() action. + * @param {TransitionCallback} cb - The callback function to invoke. + * @returns {unknown} void. + */ + registerOnConfigure(cb) { + this._callbackMap.set( + getStateInterface().TRANSITION_STATE_CONFIGURING, + cb); + } + + /** + * Register a callback function to be invoked during the activate() action. + * @param {TransitionCallback} cb - The callback function to invoke. + * @returns {unknown} void. + */ + registerOnActivate(cb) { + this._callbackMap.set( + getStateInterface().TRANSITION_STATE_ACTIVATING, + cb); + } + + /** + * Register a callback function to be invoked during the deactivate() action. + * @param {TransitionCallback} cb - The callback function to invoke. + * @returns {unknown} void. + */ + registerOnDeactivate(cb) { + this._callbackMap.set( + getStateInterface().TRANSITION_STATE_DEACTIVATING, + cb); + } + + /** + * Register a callback function to be invoked during the cleanup() action. + * @param {TransitionCallback} cb - The callback function to invoke. + * @returns {unknown} void. + */ + registerOnCleanup(cb) { + this._callbackMap.set( + getStateInterface().TRANSITION_STATE_CLEANINGUP, + cb); + } + + /** + * Register a callback function to be invoked during the shutdown() action. + * @param {TransitionCallback} cb - The callback function to invoke. + * @returns {unknown} void + */ + registerOnShutdown(cb) { + this._callbackMap.set( + getStateInterface().TRANSITION_STATE_SHUTTINGDOWN, + cb); + } + + /** + * Register a callback function to be invoked when an error occurs during a + * state transition. + * @param {TransitionCallback} cb - The callback function to invoke. + * @returns {unknown} void. + */ + registerOnError(cb) { + this._callbackMap.set( + getStateInterface().TRANSITION_STATE_ERRORPROCESSING, + cb); + } + + /** + * Initiate a transition from the UNCONFIGURED state to the INACTIVE state. + * If an onConfigure callback has been registered it will be invoked. + * + * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns {State} The new state, should be INACTIVE. + * @throws {Error} If transition is invalid for the current state. + */ + configure(callbackReturnValue) { + return this._changeState(getTransitionInterface().TRANSITION_CONFIGURE, callbackReturnValue); + } + + /** + * Initiate a transition from the INACTIVE state to the ACTIVE state. + * If an onActivate callback has been registered it will be invoked. + * + * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns {State} The new state, should be ACTIVE. + * @throws {Error} If transition is invalid for the current state. + */ + activate(callbackReturnValue) { + return this._changeState(getTransitionInterface().TRANSITION_ACTIVATE, callbackReturnValue); + } + + /** + * Initiate a transition from the ACTIVE state to the INACTIVE state. + * If an onDeactivate callback has been registered it will be invoked. + * + * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns {State} The new state, should be INACTIVE. + * @throws {Error} If transition is invalid for the current state. + */ + deactivate(callbackReturnValue) { + return this._changeState(getTransitionInterface().TRANSITION_DEACTIVATE, callbackReturnValue); + } + + /** + * Initiate a transition from the INACTIVE state to the UNCONFIGURED state. + * If an onCleanup callback has been registered it will be invoked. + * + * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns {State} The new state, should be INACTIVE. + * @throws {Error} If transition is invalid for the current state. + */ + cleanup(callbackReturnValue) { + return this._changeState(getTransitionInterface().TRANSITION_CLEANUP, callbackReturnValue); + } + + /** + * Initiate a transition to the FINALIZED state from any of the following + * states: UNCONFIGURED, INACTIVE or ACTIVE state. If an onConfigure + * callback has been registered it will be invoked. + * + * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns {State} The new state, should be FINALIZED. + * @throws {Error} If transition is invalid for the current state. + */ + shutdown(callbackReturnValue) { + let state = this.currentState; + + return this._changeState(SHUTDOWN_TRANSITION_LABEL, callbackReturnValue); + } + + /** + * The GetState service handler. + * @param {Object} request - The GetState service request. + * @param {Object} response - The GetState service response. + * @returns {unknown} void. + * @private + */ + _onGetState(request, response) { + let result = response.template; + + // eslint-disable-next-line camelcase + result.current_state = this.currentState; + + response.send(result); + } + + /** + * The GetAvailableStates service handler. + * @param {Object} request - The GetAvailableStates service request. + * @param {Object} response - The GetAvailableStates service response. + * @returns {unknown} void. + * @private + */ + _onGetAvailableStates(request, response) { + let result = response.template; + + // eslint-disable-next-line camelcase + result.available_states = this.availableStates; + + response.send(result); + } + + /** + * The GetAvailableTransitions service handler. + * @param {Object} request - The GetAvailableTransitions service request + * @param {Object} response - The GetAvailableTranactions service response. + * @returns {unknown} void. + */ + _onGetAvailableTransitions(request, response) { + let result = response.template; + + // eslint-disable-next-line camelcase + result.available_transitions = this.availableTransitions; + + response.send(result); + } + + /** + * The ChangeState service handler. + * @param {Object} request - The ChangeState service request. + * @param {Object} response - The ChangeState service response + * @returns {unknown} void. + * @private + */ + _onChangeState(request, response) { + let result = response.template; + + let transitionId = request.transition.id; + if (request.transition.label) { + let transitionObj = + rclnodejs.getLifecycleTransitionByLabel(this._stateMachineHandle, request.transition.label); + if (transitionObj.id) { + transitionId = transitionObj.id; + } else { + result.success = false; + response.send(result); + return; + } + } + + let callbackReturnValue = new CallbackReturnValue(); + this._changeState(transitionId, callbackReturnValue); + + result.success = callbackReturnValue.isSuccess(); + response.send(result); + } + + /** + * Transition to a new lifecycle state. + * @param {number|string} transitionIdOrLabel - The id or label of the target transition. + * @param {CallbackReturnValue} callbackReturnValue - An out parameter that holds the CallbackReturnCode. + * @returns {State} The new state. + * @throws {Error} If transition is invalid for the current state. + * @private + */ + _changeState(transitionIdOrLabel, callbackReturnValue) { + let initialState = this.currentState; + let newStateObj = typeof transitionIdOrLabel === 'number' ? + rclnodejs.triggerLifecycleTransitionById(this._stateMachineHandle, transitionIdOrLabel) : + rclnodejs.triggerLifecycleTransitionByLabel(this._stateMachineHandle, transitionIdOrLabel); + + if (!newStateObj) { + throw new Error(`No transition available from state ${transitionIdOrLabel}.`); + } + + let newState = State.fromSerializedState(newStateObj); + + let cbResult = this._executeCallback(newState, initialState); + if (callbackReturnValue) callbackReturnValue.value = cbResult; + + let transitioningLabel = this._transitionId2Label(cbResult); + newState = State.fromSerializedState( + rclnodejs.triggerLifecycleTransitionByLabel(this._stateMachineHandle, transitioningLabel) + ); + + if (cbResult == CallbackReturnCode.ERROR) { + cbResult = this._executeCallback(this.currentState, initialState); + if (callbackReturnValue) callbackReturnValue.value = cbResult; + + transitioningLabel = this._transitionId2Label(cbResult); + newState = State.fromSerializedState( + rclnodejs.triggerLifecycleTransitionByLabel(this._stateMachineHandle, transitioningLabel) + ); + } + + return newState; + } + + /** + * Execute the callback function registered with a transition action, + * e.g. registerOnConfigure(cb). + * @param {State} state - The state to which the callback is + * @param {State} prevState - The start state of the transition. + * @returns {CallbackReturnCode} The callback return code. + * @private + */ + _executeCallback(state, prevState) { + let result = CallbackReturnCode.SUCCESS; + let callback = this._callbackMap.get(state.id); + + if (callback) { + try { + result = callback(prevState); + } catch (err) { + console.log('CB exception occured: ', err); + result = CallbackReturnCode.ERROR; + } + } + + return result; + } + + /** + * Find the label for the transition with id == transitionId. + * @param {number} transitionId - A transition id. + * @returns {string} The label of the transition with id. + * @private + */ + _transitionId2Label(transitionId) { + return rclnodejs.getLifecycleTransitionIdToLabel(transitionId); + } +} + +const Lifecycle = { + CallbackReturnCode, + CallbackReturnValue, + LifecycleNode, + State, + Transition, + TransitionDescription +}; + +module.exports = Lifecycle; diff --git a/lib/lifecycle_publisher.js b/lib/lifecycle_publisher.js new file mode 100644 index 00000000..9b488122 --- /dev/null +++ b/lib/lifecycle_publisher.js @@ -0,0 +1,113 @@ +// Copyright (c) 2020 Wayne Parrott. All rights reserved. +// +// 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 Logging = require('./logging.js'); +const Publisher = require('./publisher.js'); + +/** + * A publisher that sends messages only when activated. + * This implementation is based on the + * {@link https://github.com/ros2/rclcpp/blob/master/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_publisher.hpp|rclcpp LifecyclePublisher class}. + * + * @hideconstructor + */ +class LifecyclePublisher extends Publisher { + + constructor(handle, typeClass, topic, options) { + super(handle, typeClass, options); + + this._enabled = false; + this._loggger = Logging.getLogger('LifecyclePublisher'); + } + + /** + * Publish a message only when activated; otherwise do nothing (nop); + * + * @param {object|Buffer} message - The message to be sent, could be kind of JavaScript message generated from .msg + * or be a Buffer for a raw message. + * @returns {undefined} + */ + publish(message) { + if (!this._enabled) { + this._loggger.warn( + `Trying to publish message on the topic ${this.topic}, but the publisher is not activated` + ); + + return; + } + + return super.publish(message); + } + + /** + * Enables communications; publish() will now send messages. + * @returns {unknown} Void return. + */ + activate() { + this._enabled = true; + } + + /** + * Disable communications; publish() will not send messages. + * @returns {unknown} Void return. + */ + deactivate() { + this._enabled = false; + } + + /** + * Determine if communications are enabled, i.e., activated, or + * disabled, i.e., deactivated. + * @returns {boolean} True if activated; otherwise false. + */ + isActivated() { + return this._enabled; + } + + /** + * A lifecycle-node activation notice. + * @returns {unknown} Void return. + */ + onActivate() { + this.activate(); + } + + /** + * A lifecycle-node deactivation notice. + * @returns {unknown} Void return. + */ + onDeactivate() { + this.deactivate(); + } + + static createPublisher(nodeHandle, typeClass, topic, options) { + let type = typeClass.type(); + let handle = rclnodejs.createPublisher( + nodeHandle, + type.pkgName, + type.subFolder, + type.interfaceName, + topic, + options.qos + ); + + return new LifecyclePublisher(handle, typeClass, topic, options); + } +} + +module.exports = LifecyclePublisher; + diff --git a/lib/node.js b/lib/node.js index 903c6f78..78960ea3 100644 --- a/lib/node.js +++ b/lib/node.js @@ -416,6 +416,10 @@ class Node { * @return {Publisher} - An instance of Publisher. */ createPublisher(typeClass, topic, options) { + return this._createPublisher(typeClass, topic, options, Publisher); + } + + _createPublisher(typeClass, topic, options, publisherClass) { if (typeof typeClass === 'string' || typeof typeClass === 'object') { typeClass = loader.loadInterface(typeClass); } @@ -425,7 +429,12 @@ class Node { throw new TypeError('Invalid argument'); } - let publisher = Publisher.createPublisher(this.handle, typeClass, topic, options); + let publisher = publisherClass.createPublisher( + this.handle, + typeClass, + topic, + options + ); debug('Finish creating publisher, topic = %s.', topic); this._publishers.push(publisher); return publisher; @@ -563,7 +572,13 @@ class Node { throw new TypeError('Invalid argument'); } - let service = Service.createService(this.handle, serviceName, typeClass, options, callback); + let service = Service.createService( + this.handle, + serviceName, + typeClass, + options, + callback + ); debug('Finish creating service, service = %s.', serviceName); this._services.push(service); this.syncHandles(); diff --git a/src/addon.cpp b/src/addon.cpp index 9d7d566c..b29d75cb 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -18,6 +18,7 @@ #include "rcl_action_bindings.hpp" #include "rcl_bindings.hpp" #include "rcl_handle.hpp" +#include "rcl_lifecycle_bindings.hpp" #include "rcutils/logging.h" #include "rcutils/macros.h" #include "shadow_node.hpp" @@ -70,6 +71,16 @@ void InitModule(v8::Local exports) { .ToLocalChecked()); } + for (uint32_t i = 0; i < rclnodejs::lifecycle_binding_methods.size(); i++) { + Nan::Set( + exports, + Nan::New(rclnodejs::lifecycle_binding_methods[i].name).ToLocalChecked(), + Nan::New( + rclnodejs::lifecycle_binding_methods[i].function) + ->GetFunction(context) + .ToLocalChecked()); + } + rclnodejs::ShadowNode::Init(exports); rclnodejs::RclHandle::Init(exports); diff --git a/src/rcl_handle.cpp b/src/rcl_handle.cpp index 9612ea9f..2c9ce503 100644 --- a/src/rcl_handle.cpp +++ b/src/rcl_handle.cpp @@ -127,7 +127,9 @@ void RclHandle::Reset() { Nan::ThrowError(rcl_get_error_string().str); rcl_reset_error(); } + free(pointer_); + pointer_ = nullptr; children_.clear(); } diff --git a/src/rcl_handle.hpp b/src/rcl_handle.hpp index 65ca6b88..b2da84c7 100644 --- a/src/rcl_handle.hpp +++ b/src/rcl_handle.hpp @@ -29,7 +29,7 @@ class RclHandle : public Nan::ObjectWrap { static void Init(v8::Local exports); static v8::Local NewInstance( void* handle, RclHandle* parent = nullptr, - std::function deleter = [] { return 0; }); + std::function deleter = [] { return 0; } ); void set_deleter(std::function deleter) { deleter_ = deleter; } diff --git a/src/rcl_lifecycle_bindings.cpp b/src/rcl_lifecycle_bindings.cpp new file mode 100644 index 00000000..ad53eee7 --- /dev/null +++ b/src/rcl_lifecycle_bindings.cpp @@ -0,0 +1,374 @@ +// Copyright (c) 2020 Wayne Parrott. All rights reserved. +// +// 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_lifecycle_bindings.hpp" + +#include +#include + +#include +#include +#include + +#include "macros.hpp" +#include "rcl_handle.hpp" +#include "rcl_utilities.hpp" +#include "lifecycle_msgs/msg/transition_event.h" +#include "lifecycle_msgs/srv/change_state.h" +#include "lifecycle_msgs/srv/get_available_states.h" +#include "lifecycle_msgs/srv/get_available_transitions.h" +#include "lifecycle_msgs/srv/get_state.h" + + +namespace rclnodejs { + +static v8::Local wrapState(const rcl_lifecycle_state_t* state) { + v8::Local jsState = Nan::New(); + Nan::Set(jsState, Nan::New("id").ToLocalChecked(), Nan::New(state->id)); + Nan::Set(jsState, Nan::New("label").ToLocalChecked(), + Nan::New(state->label).ToLocalChecked()); + return jsState; +} + +static v8::Local wrapTransition( + const rcl_lifecycle_transition_t* transition) { + v8::Local jsTransition = Nan::New(); + Nan::Set(jsTransition, Nan::New("id").ToLocalChecked(), + Nan::New(transition->id)); + Nan::Set(jsTransition, Nan::New("label").ToLocalChecked(), + Nan::New(transition->label).ToLocalChecked()); + return jsTransition; +} + +NAN_METHOD(CreateLifecycleStateMachine) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* node_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_node_t* node = reinterpret_cast(node_handle->ptr()); + const rcl_node_options_t* node_options = + reinterpret_cast(rcl_node_get_options(node)); + + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + malloc(sizeof(rcl_lifecycle_state_machine_t))); + *state_machine = rcl_lifecycle_get_zero_initialized_state_machine(); + + const rosidl_message_type_support_t* pn = + GetMessageTypeSupport("lifecycle_msgs", "msg", "TransitionEvent"); + const rosidl_service_type_support_t* cs = + GetServiceTypeSupport("lifecycle_msgs", "ChangeState"); + const rosidl_service_type_support_t* gs = + GetServiceTypeSupport("lifecycle_msgs", "GetState"); + const rosidl_service_type_support_t* gas = + GetServiceTypeSupport("lifecycle_msgs", "GetAvailableStates"); + const rosidl_service_type_support_t* gat = + GetServiceTypeSupport("lifecycle_msgs", "GetAvailableTransitions"); + const rosidl_service_type_support_t* gtg = + GetServiceTypeSupport("lifecycle_msgs", "GetAvailableTransitions"); + + THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, + rcl_lifecycle_state_machine_init( + state_machine, node, pn, cs, gs, gas, gat, gtg, + true, &node_options->allocator), + rcl_get_error_string().str); + + auto js_obj = RclHandle::NewInstance( + state_machine, node_handle, [state_machine, node, node_options] { + return rcl_lifecycle_state_machine_fini(state_machine, node, + &node_options->allocator); + }); + + info.GetReturnValue().Set(js_obj); +} + +NAN_METHOD(GetCurrentLifecycleState) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + + const rcl_lifecycle_state_t* current_state = state_machine->current_state; + info.GetReturnValue().Set(wrapState(current_state)); +} + +NAN_METHOD(GetLifecycleTransitionByLabel) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + + std::string transition_label(*Nan::Utf8String(info[1])); + + auto transition = rcl_lifecycle_get_transition_by_label( + state_machine->current_state, transition_label.c_str()); + + info.GetReturnValue().Set(transition == nullptr ? Nan::New() + : wrapTransition(transition)); +} + +// return all registered states +NAN_METHOD(GetLifecycleStates) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + + v8::Local states = Nan::New(); + + for (uint8_t i = 0; i < state_machine->transition_map.states_size; ++i) { + const rcl_lifecycle_state_t state = state_machine->transition_map.states[i]; + v8::Local jsState = wrapState(&state); + Nan::Set(states, i, jsState); + } + + info.GetReturnValue().Set(states); +} + +// return all registered transitions +NAN_METHOD(GetLifecycleTransitions) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + + v8::Local jsTransitions = Nan::New(); + + for (uint8_t i = 0; i < state_machine->transition_map.transitions_size; ++i) { + auto transition = state_machine->transition_map.transitions[i]; + v8::Local jsTransitionDesc = Nan::New(); + Nan::Set(jsTransitionDesc, Nan::New("transition").ToLocalChecked(), + wrapTransition(&transition)); + Nan::Set(jsTransitionDesc, Nan::New("start_state").ToLocalChecked(), + wrapState(transition.start)); + Nan::Set(jsTransitionDesc, Nan::New("goal_state").ToLocalChecked(), + wrapState(transition.goal)); + + Nan::Set(jsTransitions, i, jsTransitionDesc); + } + + info.GetReturnValue().Set(jsTransitions); +} + +// return the transitions available from the current state +NAN_METHOD(GetAvailableLifecycleTransitions) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + + v8::Local jsTransitions = Nan::New(); + + for (uint8_t i = 0; i < state_machine->current_state->valid_transition_size; + ++i) { + auto transition = state_machine->current_state->valid_transitions[i]; + v8::Local jsTransitionDesc = Nan::New(); + Nan::Set(jsTransitionDesc, Nan::New("transition").ToLocalChecked(), + wrapTransition(&transition)); + Nan::Set(jsTransitionDesc, Nan::New("start_state").ToLocalChecked(), + wrapState(transition.start)); + Nan::Set(jsTransitionDesc, Nan::New("goal_state").ToLocalChecked(), + wrapState(transition.goal)); + + Nan::Set(jsTransitions, i, jsTransitionDesc); + } + + info.GetReturnValue().Set(jsTransitions); +} + +NAN_METHOD(GetLifecycleSrvNameAndHandle) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* node_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_node_t* node = reinterpret_cast(node_handle->ptr()); + + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[1]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + + std::string lifecycle_srv_field_name(*Nan::Utf8String(info[2])); + + rcl_service_t* service = nullptr; + if (lifecycle_srv_field_name.compare("srv_get_state") == 0) { + service = &state_machine->com_interface.srv_get_state; + } else if (lifecycle_srv_field_name.compare("srv_get_available_states") + == 0) { + service = &state_machine->com_interface.srv_get_available_states; + } else if (lifecycle_srv_field_name.compare( + "srv_get_available_transitions") == 0) { + service = &state_machine->com_interface.srv_get_available_transitions; + } else if (lifecycle_srv_field_name.compare("srv_change_state") == 0) { + service = &state_machine->com_interface.srv_change_state; + } + + THROW_ERROR_IF_EQUAL(nullptr, + service, + "Service not found."); + + std::string service_name = rcl_service_get_service_name(service); + + // build result object {name: , handle: } + v8::Local named_srv_obj = Nan::New(); + Nan::Set(named_srv_obj, Nan::New("name").ToLocalChecked(), + Nan::New(service_name).ToLocalChecked()); + + // Note: lifecycle Services are created and managed by their + // rcl_lifecycle_state_machine. Thus we must not manually + // free the lifecycle_state_machine's service pointers. + // To accomplish this we create srv_handle instances with + // the free_ptr parameter of false. Failing to do this results + // in a double free() error. + auto srv_handle = + RclHandle::NewInstance(service, nullptr, [] { return RCL_RET_OK; }); + RclHandle* rclHandle = RclHandle::Unwrap(srv_handle); + RclHandle::Unwrap(srv_handle)->set_deleter([rclHandle] { + rclHandle->set_ptr(nullptr); + return RCL_RET_OK; + }); + + // auto srv_handle = RclHandle::NewInstance(service, + // nullptr, + // [] { return RCL_RET_OK; }, + // false); + Nan::Set(named_srv_obj, Nan::New("handle").ToLocalChecked(), srv_handle); + + info.GetReturnValue().Set(named_srv_obj); +} + +// return null if transition exists from current state +NAN_METHOD(TriggerLifecycleTransitionById) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + int transition_id = Nan::To(info[1]).FromJust(); + + bool publish = true; + + THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, + rcl_lifecycle_trigger_transition_by_id( + state_machine, transition_id, publish), + rcl_get_error_string().str); + + const rcl_lifecycle_state_t* current_state = state_machine->current_state; + info.GetReturnValue().Set(wrapState(current_state)); +} + +// return null if transition exists from current state +NAN_METHOD(TriggerLifecycleTransitionByLabel) { + v8::Local currentContent = Nan::GetCurrentContext(); + RclHandle* state_machine_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_lifecycle_state_machine_t* state_machine = + reinterpret_cast( + state_machine_handle->ptr()); + std::string transition_label(*Nan::Utf8String(info[1])); + + bool publish = true; + + THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, + rcl_lifecycle_trigger_transition_by_label( + state_machine, transition_label.c_str(), publish), + rcl_get_error_string().str); + + const rcl_lifecycle_state_t* current_state = state_machine->current_state; + info.GetReturnValue().Set(wrapState(current_state)); +} + +static const char* transitionId2Label(int callback_ret) { + if (callback_ret == + lifecycle_msgs__msg__Transition__TRANSITION_CALLBACK_SUCCESS) { + return rcl_lifecycle_transition_success_label; + } + + if (callback_ret == + lifecycle_msgs__msg__Transition__TRANSITION_CALLBACK_FAILURE) { + return rcl_lifecycle_transition_failure_label; + } + + if (callback_ret == + lifecycle_msgs__msg__Transition__TRANSITION_CALLBACK_ERROR) { + return rcl_lifecycle_transition_error_label; + } + + if (callback_ret == lifecycle_msgs__msg__Transition__TRANSITION_CONFIGURE) { + return rcl_lifecycle_configure_label; + } + + if (callback_ret == lifecycle_msgs__msg__Transition__TRANSITION_ACTIVATE) { + return rcl_lifecycle_activate_label; + } + + if (callback_ret == lifecycle_msgs__msg__Transition__TRANSITION_DEACTIVATE) { + return rcl_lifecycle_deactivate_label; + } + + if (callback_ret == lifecycle_msgs__msg__Transition__TRANSITION_CLEANUP) { + return rcl_lifecycle_cleanup_label; + } + + if (callback_ret == + lifecycle_msgs__msg__Transition__TRANSITION_UNCONFIGURED_SHUTDOWN || + callback_ret == + lifecycle_msgs__msg__Transition__TRANSITION_INACTIVE_SHUTDOWN || + callback_ret == + lifecycle_msgs__msg__Transition__TRANSITION_ACTIVE_SHUTDOWN) { + return rcl_lifecycle_shutdown_label; + } + + return rcl_lifecycle_transition_error_label; +} + +NAN_METHOD(GetLifecycleTransitionIdToLabel) { + v8::Local currentContent = Nan::GetCurrentContext(); + int callback_ret = Nan::To(info[0]).FromJust(); + const char* transition_label = transitionId2Label(callback_ret); + info.GetReturnValue().Set(Nan::New(transition_label).ToLocalChecked()); +} + +NAN_METHOD(GetLifecycleShutdownTransitionLabel) { + v8::Local currentContent = Nan::GetCurrentContext(); + int callback_ret = Nan::To(info[0]).FromJust(); + info.GetReturnValue().Set( + Nan::New(rcl_lifecycle_shutdown_label).ToLocalChecked()); +} + +std::vector lifecycle_binding_methods = { + {"createLifecycleStateMachine", CreateLifecycleStateMachine}, + {"getCurrentLifecycleState", GetCurrentLifecycleState}, + {"getLifecycleTransitionByLabel", GetLifecycleTransitionByLabel}, + {"getLifecycleStates", GetLifecycleStates}, + {"getLifecycleTransitions", GetLifecycleTransitions}, + {"getAvailableLifecycleTransitions", GetAvailableLifecycleTransitions}, + {"triggerLifecycleTransitionById", TriggerLifecycleTransitionById}, + {"triggerLifecycleTransitionByLabel", TriggerLifecycleTransitionByLabel}, + {"getLifecycleSrvNameAndHandle", GetLifecycleSrvNameAndHandle}, + {"getLifecycleTransitionIdToLabel", GetLifecycleTransitionIdToLabel}, + {"getLifecycleShutdownTransitionLabel", + GetLifecycleShutdownTransitionLabel}}; + +} // namespace rclnodejs diff --git a/src/rcl_lifecycle_bindings.hpp b/src/rcl_lifecycle_bindings.hpp new file mode 100644 index 00000000..2f84cb19 --- /dev/null +++ b/src/rcl_lifecycle_bindings.hpp @@ -0,0 +1,28 @@ +// Copyright (c) 2020 Wayne Parrott. All rights reserved. +// +// 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 RCLNODEJS_RCL_LIFECYCLE_BINDINGS_HPP_ +#define RCLNODEJS_RCL_LIFECYCLE_BINDINGS_HPP_ + +#include + +#include "rcl_bindings.hpp" + +namespace rclnodejs { + +extern std::vector lifecycle_binding_methods; + +} // namespace rclnodejs + +#endif diff --git a/test/test-lifecycle-publisher.js b/test/test-lifecycle-publisher.js new file mode 100644 index 00000000..4be91f2d --- /dev/null +++ b/test/test-lifecycle-publisher.js @@ -0,0 +1,109 @@ +// +// 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 rclnodejs = require('../index.js'); +const assertUtils = require('./utils.js'); +const assertThrowsError = assertUtils.assertThrowsError; + +const NODE_NAME = 'lifecycle_node'; + +// let StateInterface = rclnodejs.createMessage('lifecycle_msgs/msg/State').constructor; +// let TransitionInterface = rclnodejs.createMessage('lifecycle_msgs/msg/Transition').constructor; + +describe('LifecyclePublisher test suite', function () { + let node; + this.timeout(60 * 1000); + + before(async function () { + await rclnodejs.init(); + }); + + beforeEach(function () { + node = rclnodejs.createLifecycleNode(NODE_NAME); + rclnodejs.spin(node); + }); + + afterEach(function () { + node.destroy(); + }); + + after(function () { + rclnodejs.shutdown(); + }); + + it('LifecyclePublisher create', function () { + const publisher = node.createPublisher('std_msgs/msg/String', 'test'); + assert.ok(publisher); + + const lifecyclePublisher = node.createLifecyclePublisher('std_msgs/msg/String', 'test'); + assert.ok(lifecyclePublisher); + + }); + + it('LifecyclePublisher activate/deactivate', function () { + const lifecyclePublisher = node.createLifecyclePublisher('std_msgs/msg/String', 'test'); + assert.ok(lifecyclePublisher); + + assert.ok(!lifecyclePublisher.isActivated()); + + lifecyclePublisher.activate(); + assert.ok(lifecyclePublisher.isActivated()); + + lifecyclePublisher.deactivate(); + assert.ok(!lifecyclePublisher.isActivated()); + + lifecyclePublisher.onActivate(); + assert.ok(lifecyclePublisher.isActivated()); + + lifecyclePublisher.onDeactivate(); + assert.ok(!lifecyclePublisher.isActivated()); + }); + + it('LifecyclePublisher publish', async function () { + const lifecyclePublisher = node.createLifecyclePublisher('std_msgs/msg/String', 'test'); + assert.ok(lifecyclePublisher); + + const TEST_MSG = 'test-msg'; + const waitTime = 1000; // millis + let cbCnt = 0; + let subscriber = node.createSubscription('std_msgs/msg/String', 'test', + (msg) => { + cbCnt++; + assert.equal(msg.data, TEST_MSG); + }); + + lifecyclePublisher.publish(TEST_MSG); + await assertUtils.createDelay(waitTime); + assert.strictEqual(cbCnt, 0); + + lifecyclePublisher.activate(); + lifecyclePublisher.publish(TEST_MSG); + await assertUtils.createDelay(waitTime); + assert.strictEqual(cbCnt, 1); + + lifecyclePublisher.deactivate(); + lifecyclePublisher.publish(TEST_MSG); + await assertUtils.createDelay(waitTime); + assert.strictEqual(cbCnt, 1); + + lifecyclePublisher.activate(); + lifecyclePublisher.publish(TEST_MSG); + await assertUtils.createDelay(waitTime); + assert.strictEqual(cbCnt, 2); + }); + +}); diff --git a/test/test-lifecycle.js b/test/test-lifecycle.js new file mode 100644 index 00000000..aa13f70f --- /dev/null +++ b/test/test-lifecycle.js @@ -0,0 +1,277 @@ +// +// 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 rclnodejs = require('../index.js'); +const assertUtils = require('./utils.js'); +const assertThrowsError = assertUtils.assertThrowsError; + +const NODE_NAME = 'lifecycle_node'; + +let StateInterface; +let TransitionInterface; + +function verifyAvailableStates(states) { + assert.equal(states.length, 11); + + // Primary States + assert.equal(states[0].id, 0); + assert.equal(states[1].id, StateInterface.PRIMARY_STATE_UNCONFIGURED); + assert.equal(states[2].id, StateInterface.PRIMARY_STATE_INACTIVE); + assert.equal(states[3].id, StateInterface.PRIMARY_STATE_ACTIVE); + assert.equal(states[4].id, StateInterface.PRIMARY_STATE_FINALIZED); + + // Transition States + assert.equal(states[5].id, StateInterface.TRANSITION_STATE_CONFIGURING); + assert.equal(states[6].id, StateInterface.TRANSITION_STATE_CLEANINGUP); + assert.equal(states[7].id, StateInterface.TRANSITION_STATE_SHUTTINGDOWN); + assert.equal(states[8].id, StateInterface.TRANSITION_STATE_ACTIVATING); + assert.equal(states[9].id, StateInterface.TRANSITION_STATE_DEACTIVATING); + assert.equal(states[10].id, StateInterface.TRANSITION_STATE_ERRORPROCESSING); +} + +describe('LifecycleNode test suite', function () { + let node; + this.timeout(60 * 1000); + + before(async function () { + await rclnodejs.init(); + StateInterface = rclnodejs.createMessage('lifecycle_msgs/msg/State').constructor; + TransitionInterface = rclnodejs.createMessage('lifecycle_msgs/msg/Transition').constructor; + }); + + beforeEach(function () { + node = rclnodejs.createLifecycleNode(NODE_NAME); + rclnodejs.spin(node); + }); + + afterEach(function () { + node.destroy(); + }); + + after(function () { + rclnodejs.shutdown(); + }); + + it('lifecycleNode initial state', function () { + const state = node.currentState; + assert.equal(state.id, StateInterface.PRIMARY_STATE_UNCONFIGURED); + }); + + it('lifecycleNode transitions', function () { + assert.equal(node.currentState.id, StateInterface.PRIMARY_STATE_UNCONFIGURED); + + let state = node.configure(); + assert.equal(node.currentState.id, StateInterface.PRIMARY_STATE_INACTIVE); + assert.equal(state.id, StateInterface.PRIMARY_STATE_INACTIVE); + + state = node.activate(); + assert.equal(node.currentState.id, StateInterface.PRIMARY_STATE_ACTIVE); + assert.equal(state.id, StateInterface.PRIMARY_STATE_ACTIVE); + + state = node.deactivate(); + assert.equal(node.currentState.id, StateInterface.PRIMARY_STATE_INACTIVE); + assert.equal(state.id, StateInterface.PRIMARY_STATE_INACTIVE); + + state = node.cleanup(); + assert.equal(node.currentState.id, StateInterface.PRIMARY_STATE_UNCONFIGURED); + assert.equal(state.id, StateInterface.PRIMARY_STATE_UNCONFIGURED); + + state = node.shutdown(); + assert.equal(node.currentState.id, StateInterface.PRIMARY_STATE_FINALIZED); + assert.equal(state.id, StateInterface.PRIMARY_STATE_FINALIZED); + }); + + it('lifecycleNode getAvailableStates', function () { + const states = node.availableStates; + verifyAvailableStates(states); + }); + + it('lifecycleNode callback registration and invoke', function () { + let cbCnt = 0; + + let cb = (prevState) => { + cbCnt++; + return rclnodejs.lifecycle.CallbackReturnCode.SUCCESS; + }; + + node.registerOnConfigure(cb); + node.registerOnActivate(cb); + node.registerOnDeactivate(cb); + node.registerOnCleanup(cb); + node.registerOnShutdown(cb); + node.registerOnError(cb); + + node.configure(); + node.activate(); + node.deactivate(); + node.cleanup(); + node.shutdown(); + + assert.equal(cbCnt, 5); + }); + + it('lifecycleNode fail transition', function () { + assert.throws(node.activate); + }); + + it('lifecycleNode onError callback', function () { + + let failCb = (prevState) => { + return rclnodejs.lifecycle.CallbackReturnCode.ERROR; + }; + + node.registerOnConfigure(failCb); + + let errorCbCnt = 0; + node.registerOnError((prevState) => errorCbCnt++); + + node.configure(); + assert.equal(errorCbCnt, 1); + }); + + it('lifecycle event publisher', async function () { + let eventCnt = 0; + + let subscription = + node.createSubscription( + 'lifecycle_msgs/msg/TransitionEvent', + '~/transition_event', + (msg) => { + eventCnt++; + }); + + node.configure(); + await assertUtils.createDelay(1000); + + assert.equal(eventCnt, 2); + }); + + it('LifecycleNode srv/GetState', async function () { + node.configure(); + + let client = + node.createClient( + 'lifecycle_msgs/srv/GetState', + '~/get_state'); + + let currentState; + client.waitForService(1000).then(result => { + if (!result) { + assert.fail('Error: GetState service not available'); + } + client.sendRequest({}, response => { + currentState = response.current_state; + }); + }); + + await assertUtils.createDelay(1000); + + assert.ok(currentState); + assert.equal(currentState.id, StateInterface.PRIMARY_STATE_INACTIVE); + }); + + it('LifecycleNode srv/GetAvailableStates', async function () { + let client = + node.createClient( + 'lifecycle_msgs/srv/GetAvailableStates', + '~/get_available_states'); + + let states; + client.waitForService(1000).then(result => { + if (!result) { + assert.fail('Error: GetAvailableStates service not available'); + } + client.sendRequest({}, response => { + states = response.available_states; + }); + }); + + await assertUtils.createDelay(1000); + + assert.ok(states); + verifyAvailableStates(states); + }); + + it('LifecycleNode srv/GetAvailableTransitions', async function () { + let client = + node.createClient( + 'lifecycle_msgs/srv/GetAvailableTransitions', + '~/get_available_transitions'); + + let transitions; + client.waitForService(1000).then(result => { + if (!result) { + assert.fail('Error: GetAvailableTransitions service not available'); + } + client.sendRequest({}, response => { + transitions = response.available_transitions; + }); + }); + + await assertUtils.createDelay(1000); + + assert.ok(transitions); + assert.strictEqual(transitions.length, 2); + assert.ok( + transitions[0].transition.id === 1 || // configure + transitions[0].transition.id === 5); // shutdown + assert.ok( + transitions[1].transition.id === 1 || // configure + transitions[1].transition.id === 5); // shutdown + + node.configure(); + + transitions = node.availableTransitions; + assert.ok(transitions); + assert.strictEqual(transitions.length, 3); + assert.ok( + transitions[0].transition.id === 2 || // cleanup + transitions[0].transition.id === 3 || // activate + transitions[0].transition.id === 6); // shutdown + assert.ok( + transitions[1].transition.id === 2 || // cleanup + transitions[1].transition.id === 3 || // activate + transitions[1].transition.id === 6); // shutdown + assert.ok( + transitions[1].transition.id === 2 || // cleanup + transitions[1].transition.id === 3 || // activate + transitions[1].transition.id === 6); // shutdown + }); + + it('LifecycleNode srv/ChangeState', async function () { + let client = + node.createClient( + 'lifecycle_msgs/srv/ChangeState', + '~/change_state'); + + let isSuccess = false; + client.waitForService(1000).then(result => { + if (!result) { + assert.fail('Error: ChangeState service not available'); + } + let request = {transition: {id: TransitionInterface.TRANSITION_CONFIGURE, label: 'configure'}}; + client.sendRequest(request, response => { + isSuccess = response.success; + }); + }); + + await assertUtils.createDelay(1000); + + assert.ok(isSuccess); + }); + +}); diff --git a/test/types/main.ts b/test/types/main.ts index 71bac056..e6aa71b3 100644 --- a/test/types/main.ts +++ b/test/types/main.ts @@ -2,6 +2,7 @@ import * as rclnodejs from 'rclnodejs'; const NODE_NAME = 'test_node'; +const LIFECYCLE_NODE_NAME = 'lifecycle_test_node'; const TYPE_CLASS = 'std_msgs/msg/String'; const TOPIC = 'topic'; const MSG = rclnodejs.createMessageObject(TYPE_CLASS); @@ -86,6 +87,43 @@ node.countPublishers(TOPIC); // $ExpectType number node.countSubscribers(TOPIC); +// ---- LifecycleNode ---- +// $ExpectType LifecycleNode +const lifecycleNode = rclnodejs.createLifecycleNode(LIFECYCLE_NODE_NAME); + +// $ExpectType State +lifecycleNode.currentState; + +// $ExpectType State[] +lifecycleNode.availableStates; + +// $ExpectType TransitionDescription[] +lifecycleNode.transitions; + +// $ExpectType TransitionDescription[] +lifecycleNode.availableTransitions; + +//// $ExpectType TransitionCallback +// const lifecycleCB: TransitionCallback = (prevState: State) => CallbackReturnCode.SUCCESS; + +// $ExpectType CallbackReturnValue +const ReturnValue = new rclnodejs.lifecycle.CallbackReturnValue(); + +// $ExpectType State +lifecycleNode.configure(ReturnValue); + +// $ExpectType State +lifecycleNode.activate(); + +// $ExpectType State +lifecycleNode.deactivate(); + +// $ExpectType State +lifecycleNode.cleanup(); + +// $ExpectType State +lifecycleNode.shutdown(); + // ---- Publisher ---- // $ExpectType Publisher<"std_msgs/msg/String"> const publisher = node.createPublisher(TYPE_CLASS, TOPIC); @@ -114,6 +152,12 @@ publisher.publish(Buffer.from('Hello ROS World')); // $ExpectType void node.destroyPublisher(publisher); +// ---- LifecyclePublisher ---- +// $ExpectType LifecyclePublisher<"std_msgs/msg/String"> +const lifecyclePublisher = lifecycleNode.createLifecyclePublisher(TYPE_CLASS, TOPIC); + +// $ExpectType boolean +lifecyclePublisher.isActivated(); // ---- Subscription ---- // $ExpectType Subscription @@ -144,7 +188,10 @@ service.options; // ---- Client ---- // $ExpectType Client<"example_interfaces/srv/AddTwoInts"> -const client = node.createClient('example_interfaces/srv/AddTwoInts', 'add_two_ints'); +const client = node.createClient( + 'example_interfaces/srv/AddTwoInts', + 'add_two_ints' +); // $ExpectType string client.serviceName; @@ -319,46 +366,61 @@ logger.fatal('test msg'); // FOXY-ONLY, example_interfaces introduced with foxy release // ---- Int8Array ---- -const i8arr = rclnodejs.require('example_interfaces.msg.Int8MultiArray') as rclnodejs.example_interfaces.msg.Int8MultiArray; +const i8arr = rclnodejs.require( + 'example_interfaces.msg.Int8MultiArray' +) as rclnodejs.example_interfaces.msg.Int8MultiArray; // $ExpectType number[] | Int8Array i8arr.data; // ---- Uint8Array ---- -const u8arr = rclnodejs.require('example_interfaces.msg.UInt8MultiArray') as rclnodejs.example_interfaces.msg.UInt8MultiArray; +const u8arr = rclnodejs.require( + 'example_interfaces.msg.UInt8MultiArray' +) as rclnodejs.example_interfaces.msg.UInt8MultiArray; // $ExpectType number[] | Uint8Array u8arr.data; // ---- Int16Array ---- -const i16arr = rclnodejs.require('example_interfaces.msg.Int16MultiArray') as rclnodejs.example_interfaces.msg.Int16MultiArray; +const i16arr = rclnodejs.require( + 'example_interfaces.msg.Int16MultiArray' +) as rclnodejs.example_interfaces.msg.Int16MultiArray; // $ExpectType number[] | Int16Array i16arr.data; // ---- Uint16Array ---- -const u16arr = rclnodejs.require('example_interfaces.msg.UInt16MultiArray') as rclnodejs.example_interfaces.msg.UInt16MultiArray; +const u16arr = rclnodejs.require( + 'example_interfaces.msg.UInt16MultiArray' +) as rclnodejs.example_interfaces.msg.UInt16MultiArray; // $ExpectType number[] | Uint16Array u16arr.data; // ---- Int32Array ---- -const i32arr = rclnodejs.require('example_interfaces.msg.Int32MultiArray') as rclnodejs.example_interfaces.msg.Int32MultiArray; +const i32arr = rclnodejs.require( + 'example_interfaces.msg.Int32MultiArray' +) as rclnodejs.example_interfaces.msg.Int32MultiArray; // $ExpectType number[] | Int32Array i32arr.data; // ---- Uint16Array ---- -const u32arr = rclnodejs.require('example_interfaces.msg.UInt32MultiArray') as rclnodejs.example_interfaces.msg.UInt32MultiArray; +const u32arr = rclnodejs.require( + 'example_interfaces.msg.UInt32MultiArray' +) as rclnodejs.example_interfaces.msg.UInt32MultiArray; // $ExpectType number[] | Uint32Array u32arr.data; // ---- Float32Array ---- -const f32arr = rclnodejs.require('example_interfaces.msg.Float32MultiArray') as rclnodejs.example_interfaces.msg.Float32MultiArray; +const f32arr = rclnodejs.require( + 'example_interfaces.msg.Float32MultiArray' +) as rclnodejs.example_interfaces.msg.Float32MultiArray; // $ExpectType number[] | Float32Array f32arr.data; // ---- Float64Array ---- -const f64arr = rclnodejs.require('example_interfaces.msg.Float64MultiArray') as rclnodejs.example_interfaces.msg.Float64MultiArray; +const f64arr = rclnodejs.require( + 'example_interfaces.msg.Float64MultiArray' +) as rclnodejs.example_interfaces.msg.Float64MultiArray; // $ExpectType number[] | Float64Array f64arr.data; - // $ExpectType FibonacciConstructor const Fibonacci = rclnodejs.require('rclnodejs_test_msgs/action/Fibonacci'); @@ -382,7 +444,7 @@ actionClient.destroy(); // $ExpectType Promise> const goalHandlePromise = actionClient.sendGoal(new Fibonacci.Goal()); -goalHandlePromise.then(goalHandle => { +goalHandlePromise.then((goalHandle) => { // $ExpectType boolean goalHandle.accepted; @@ -461,5 +523,3 @@ function executeCallback( return new Fibonacci.Result(); } - - diff --git a/types/base.d.ts b/types/base.d.ts index 915bc181..4931b3c5 100644 --- a/types/base.d.ts +++ b/types/base.d.ts @@ -9,6 +9,8 @@ /// /// /// +/// +/// /// /// /// diff --git a/types/index.d.ts b/types/index.d.ts index ea01526b..f15e8a04 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -21,6 +21,22 @@ declare module 'rclnodejs' { options?: NodeOptions ): Node; + /** + * Create a managed Node that implements a well-defined life-cycle state + * model using the {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}. + * @param nodeName - The name used to register in ROS. + * @param namespace - The namespace used in ROS, default is an empty string. + * @param context - The context, default is Context.defaultContext(). + * @param options - The options to configure the new node behavior. + * @returns The instance of LifecycleNode. + */ + function createLifecycleNode( + nodeName: string, + namespace?: string, + context?: Context, + options?: NodeOptions + ): lifecycle.LifecycleNode; + /** * Init the module. * diff --git a/types/lifecycle.d.ts b/types/lifecycle.d.ts new file mode 100644 index 00000000..784ad8ba --- /dev/null +++ b/types/lifecycle.d.ts @@ -0,0 +1,325 @@ + +declare module 'rclnodejs' { + + namespace lifecycle { + + /** + * A simple object representation of State. + */ + interface SerializedState { + id: number; + label: string; + } + + /** + * The state of the lifecycle state-model. + */ + class State { + + /** + * Create a state. + * @param id - The id value. + * @param label - The label value. + */ + constructor(id: number, label: string); + + /** + * Create an object representation of state properties. + * @return The object. + */ + asMessage(): SerializedState; + } + + /** + * The intermediate state of the lifecycle state-model during a state + * transition process. + */ + class Transition extends State { + } + + /** + * Describes a state transition. + */ + class TransitionDescription { + + /** + * Create a transition description. + * + * @param transition - The transition + * @param startState - The initial + * @param goalState - The target state of a transition activity + */ + constructor(transition: Transition, startState: State, goalState: State); + + /** + * Create an object representation of the transitionDescripton properties. + * + * @returns The object representation. + */ + asMessage(): { + transition: SerializedState, + + // eslint-disable-next-line camelcase + start_state: SerializedState, + + // eslint-disable-next-line camelcase + goal_state: SerializedState + } + } + + /** + * The values returned by TransitionCallback. + */ + const enum CallbackReturnCode { + SUCCESS = 97, // rclnodejs.lifecycle_msgs.msg.TransitionConstructor.TRANSITION_CALLBACK_SUCCESS, + FAILURE = 98, // rclnodejs.lifecycle_msgs.msg.TransitionConstructor.TRANSITION_CALLBACK_FAILURE, + ERROR = 99 // rclnodejs.lifecycle_msgs.msg.TransitionConstructor.TRANSITION_CALLBACK_ERROR + } + + /** + * A CallbackReturnCode value-holder that is passed as an 'out' parameter + * to a LifecycleNode's transition actions, e.g., configure(). + */ + class CallbackReturnValue { + + /** + * Creates a new instance. + * + * @param value - Optional value, default = CallbackReturnCode.SUCCESS + */ + constructor(value?: CallbackReturnCode); + + /** + * Access the callbackReturnCode. + * @returns {number} The CallbackReturnCode. + */ + get value(): CallbackReturnCode; + + /** + * Assign the callbackReturnCode. + * @param value - The new value. + */ + set value(value: CallbackReturnCode); + + /** + * Access an optional error message when value is not SUCCESS. + * @returns The errorMsg or undefined if no error message has ben assigned. + */ + get errorMsg(): string; + + /** + * Assign the error message. + * @param msg - The error message. + */ + set errorMsg(msg: string); + + /** + * Overrides Object.valueOf() to return the 'value' property. + * @returns The property value. + */ + valueOf(): CallbackReturnCode; + + /** + * A predicate to test if the value is SUCCESS. + * @returns true if the value is SUCCESS; otherwise return false. + */ + isSuccess(): boolean; + + /** + * A predicate to test if the value is FAILURE. + * @returns true if the value is FAILURE; otherwise return false. + */ + isFailure(): boolean; + + /** + * A predicate to test if the value is ERROR. + * @returns true if the value is ERROR; otherwise return false. + */ + isError(): boolean + + /** + * A predicate to test if an error message has been assigned. + * @returns true if an error message has been assigned; otherwise return false. + */ + hasErrorMsg(): boolean; + } + + /** + * The callback function invoked during a lifecycle state transition. + * + * @param prevState - The state transitioning from. + * @return The return code. + */ + type TransitionCallback = (prevState: State) => CallbackReturnCode; + + /** + * A ROS2 managed Node that implements a well-defined life-cycle state-model using the + * {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}. + * + * This class implments the ROS2 life-cycle state-machine defined by the + * {@link https://github.com/ros2/rclcpp/tree/master/rclcpp_lifecycle}|ROS2 Managed Nodes Design} + * and parallels the {@link https://github.com/ros2/rclcpp/tree/master/rclcpp_lifecycle|rclcpp lifecycle node } + * implementation. + * + * The design consists of four primary lifecycle states: + * UNCONFIGURED + * INACTIVE + * ACTIVE + * FINALIZED. + * + * Transitioning between states is accomplished using an action api: + * configure() + * activate() + * deactivate(), + * cleanup() + * shutdown() + * + * During a state transition, the state-machine is in one of the + * intermediate transitioning states: + * CONFIGURING + * ACTIVATING + * DEACTIVATING + * CLEANINGUP + * SHUTTING_DOWN + * ERROR_PROCESSING + * + * Messaging: + * State changes are published on the '/transition_event' topic. + * Lifecycle service interfaces are also implemented. + * + * You can introduce your own state specific behaviors in the form of a + * {@link TransitionCallback} functions that you register using: + * registerOnConfigure(cb) + * registerOnActivate(cb) + * registerOnDeactivate(cb) + * registerOnCleanup(cb) + * registerOnShutdown(cb) + * registerOnError(cb) + */ + class LifecycleNode extends Node { + + /** + * Access the current lifecycle state. + * @returns The current state. + */ + get currentState(): State; + + /** + * Retrieve all states from the current state. + * @returns {State[]} All states of the state-machine. + */ + get availableStates(): State[]; + + /** + * Retrieve all transitions registered with the state-machine. + * + * @returns The TransitionDescriptions. + */ + get transitions(): TransitionDescription[]; + + /** + * Retrieve all transitions available from the current state of the state-machine. + * + * @returns The available transitions. + */ + get availableTransitions(): TransitionDescription[]; + + /** + * Register a callback function to be invoked during the configure() action. + * @param cb - The callback function to invoke. + */ + registerOnConfigure(cb: TransitionCallback): void; + + /** + * Register a callback function to be invoked during the activate() action. + * @param cb - The callback function to invoke. + */ + registerOnActivate(cb: TransitionCallback): void; + + /** + * Register a callback function to be invoked during the deactivate() action. + * @param cb - The callback function to invoke. + */ + registerOnDectivate(cb: TransitionCallback): void; + + /** + * Register a callback function to be invoked during the cleanup() action. + * @param cb - The callback function to invoke. + */ + registerOnCleanup(cb: TransitionCallback): void; + + /** + * Register a callback function to be invoked during the shutdown() action. + * @param cb - The callback function to invoke. + */ + registerOnShutdown(cb: TransitionCallback): void; + + /** + * Register a callback function to be invoked when an error occurs during a + * state transition. + * @param cb - The callback function to invoke. + */ + registerOnError(cb: TransitionCallback): void; + + /** + * Initiate a transition from the UNCONFIGURED state to the INACTIVE state. + * If an onConfigure callback has been registered, it will be invoked. + * + * @param callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns The new state, should be INACTIVE. + */ + configure(callbackReturnValue?: CallbackReturnValue): State; + + /** + * Initiate a transition from the INACTIVE state to the ACTIVE state. + * If an onActivate callback has been registered it will be invoked. + * + * @param callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns The new state, should be ACTIVE. + */ + activate(callbackReturnValue?: CallbackReturnValue): State; + + /** + * Initiate a transition from the ACTIVE state to the INACTIVE state. + * If an onDeactivate callback has been registered it will be invoked. + * + * @param callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns The new state, should be INACTIVE. + */ + deactivate(callbackReturnValue?: CallbackReturnValue): State; + + /** + * Initiate a transition from the INACTIVE state to the UNCONFIGURED state. + * If an onCleanup callback has been registered it will be invoked. + * + * @param callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns The new state, should be INACTIVE. + */ + cleanup(callbackReturnValue?: CallbackReturnValue): State; + + /** + * Initiate a transition from the ACTIVE state to the FINALIZED state. + * If an onConfigure callback has been registered it will be invoked. + * + * @param callbackReturnValue - value holder for the CallbackReturnCode returned from the callback. + * @returns The new state, should be FINALIZED. + */ + shutdown(callbackReturnValue?: CallbackReturnValue): State; + + /** + * Create a LifecyclePublisher. + * + * @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 + * @returns New instance of LifecyclePublisher. + */ + createLifecyclePublisher>( + typeClass: T, + topic: string, + options?: Options + ): LifecyclePublisher; + } + + } // lifecycle namespace +} // rclnodejs namespace diff --git a/types/lifecycle_publisher.d.ts b/types/lifecycle_publisher.d.ts new file mode 100644 index 00000000..2ca52c45 --- /dev/null +++ b/types/lifecycle_publisher.d.ts @@ -0,0 +1,42 @@ + +declare module 'rclnodejs' { + + namespace lifecycle { + + /** + * A publisher that sends messages only when activated. + * This implementation is based on the + * {@link https://github.com/ros2/rclcpp/blob/master/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_publisher.hpp | rclcpp LifecyclePublisher class} + */ + interface LifecyclePublisher> + extends Publisher { + + /** + * Enables communications; publish() will now send messages. + */ + activate(): void + + /** + * Disable communications; publish() will not send messages. + */ + deactivate(): void; + + /** + * Determine if communications are enabled, i.e., activated, or + * disabled, i.e., deactivated. + * @returns True if activated; otherwise false. + */ + isActivated(): boolean; + + /** + * Enables communications; publish() will now send messages. + */ + onActivate(): void; + + /** + * Disable communications; publish() will not send messages. + */ + onDeactivate(): void; + } + } +}