diff --git a/example.js b/example.js new file mode 100644 index 0000000..e1b2d5f --- /dev/null +++ b/example.js @@ -0,0 +1,35 @@ +const rosnodejs = require('rosnodejs'); +const ActionServerInterface = require('rosnodejs/dist/lib/ActionServerInterface'); +const ActionClientInterface = require('rosnodejs/dist/lib/ActionClientInterface'); + +const ActionLib = require('./index.js'); + +ActionLib.config({ + time: rosnodejs.Time, + log: rosnodejs.log.getLogger('actionlibjs'), + messages: { + getMessage(fullName) { + const [pkg, name] = fullName.split('/'); + return rosnodejs.require(pkg).msg[name] + }, + getMessageConstants(fullName) { + return this.getMessage(fullName).CONSTANTS; + } + }, + ActionServerInterface, + ActionClientInterface +}); + +rosnodejs.initNode('/tmp') +.then(() => { + const as = new ActionLib.ActionServer({ + type: 'intera_motion_msgs/MotionCommand', + actionServer: '/motion', + nh: rosnodejs.nh + }); + + +}) +.catch((err) => { + console.error(err.stack); +}) diff --git a/index.js b/index.js index 0fd476c..f9dd410 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,25 @@ -const ActionServer = require('./action/ActionServer.js'); +const ActionServer = require('./lib/ActionServer.js'); +const ActionConfig = require('./lib/ActionConfig') -module.exports = { - ActionServer +const ActionLib = { + config(configuration) { + ActionConfig.init(configuration); + } }; + +function addGuardedGetter(propertyName, val) { + Object.defineProperty(ActionLib, propertyName, { + get: function() { + if (ActionConfig.isConfigured()) { + return val; + } + else { + throw new Error(`Unable to get propertyName before actionlib has been configured`); + } + } + }); +} + +addGuardedGetter('ActionServer', ActionServer); + +module.exports = ActionLib; diff --git a/lib/ActionConfig.js b/lib/ActionConfig.js new file mode 100644 index 0000000..5ee72ab --- /dev/null +++ b/lib/ActionConfig.js @@ -0,0 +1,88 @@ + +const EventEmitter = require('events').EventEmitter; + +let ACTION_CONF = {}; +let configured = false; + +// make this so conf can alert when its configured +const exportItem = new EventEmitter(); + +const FIELDS_FORMAT = [ + 'log', + { + name: 'time', + format: [ + 'isZeroTime', + 'timeComp', + 'now', + 'toNumber', + 'epoch' + ] + }, + { + name: 'messages', + format: [ + 'getMessage', + 'getMessageConstants' + ] + }, + 'ActionServerInterface', + 'ActionClientInterface' +]; + +function reset() { + configured = false; +} + +function readConfig(conf, formatFields, writeConf, keyPath = '') { + if (!formatFields) { + throw new Error('Unable to readConfig without format at ' + keyPath); + } + else if (!conf) { + throw new Error('Invalid config - missing entry at ' + keyPath); + } + + for (let i = 0; i < formatFields.length; ++i) { + const formatField = formatFields[i]; + + if (typeof formatField === 'string') { + const confVal = conf[formatField]; + if (confVal === undefined) { + const fullPath = keyPath ? keyPath + '.' + formatField : formatField; + throw new Error('Unable to readConfig without field ' + fullPath); + } + + writeConf[formatField] = confVal; + } + else if (typeof formatField === 'object') { + const { name, format } = formatField; + if (!writeConf.hasOwnProperty(name)) { + writeConf[name] = {}; + } + + const fullPath = keyPath ? keyPath + '.' + name : name; + readConfig(conf[name], format, writeConf[name], fullPath); + } + } +} + +function init(conf) { + readConfig(conf, FIELDS_FORMAT, ACTION_CONF); + configured = true; + exportItem.emit('configured', ACTION_CONF); +} + +function isConfigured() { + return configured; +} + +function get() { + return ACTION_CONF; +} + +exportItem.init = init; +exportItem.reset = reset; +exportItem.isConfigured = isConfigured; +exportItem.get = get; + +module.exports = exportItem; diff --git a/actions/ActionServer.js b/lib/ActionServer.js similarity index 78% rename from actions/ActionServer.js rename to lib/ActionServer.js index 8c21f4b..3c17811 100644 --- a/actions/ActionServer.js +++ b/lib/ActionServer.js @@ -17,19 +17,12 @@ 'use strict'; -const timeUtils = require('../lib/Time.js'); -const msgUtils = require('../utils/message_utils.js'); +const ActionConfig = require('./ActionConfig.js'); + const EventEmitter = require('events'); -const ActionServerInterface = require('../lib/ActionServerInterface.js'); const GoalHandle = require('./GoalHandle.js'); -let GoalIdMsg = null; -let GoalStatusMsg = null; -let GoalStatusArrayMsg = null; -let GoalStatuses = null; -let goalCount = 0; - /** * @class ActionServer * EXPERIMENTAL @@ -39,28 +32,7 @@ class ActionServer extends EventEmitter { constructor(options) { super(); - if (GoalStatusMsg === null) { - GoalStatusMsg = msgUtils.requireMsgPackage('actionlib_msgs').msg.GoalStatus; - GoalStatuses = GoalStatusMsg.Constants; - } - - if (GoalStatusArrayMsg === null) { - GoalStatusArrayMsg = msgUtils.requireMsgPackage('actionlib_msgs').msg.GoalStatusArray; - } - - this._asInterface = new ActionServerInterface(options); - - this._asInterface.on('goal', this._handleGoal.bind(this)); - this._asInterface.on('cancel', this._handleCancel.bind(this)); - - const actionType = this._asInterface.getType(); - - this._messageTypes = { - result: msgUtils.getHandlerForMsgType(actionType + 'Result'), - feedback: msgUtils.getHandlerForMsgType(actionType + 'Feedback'), - actionResult: msgUtils.getHandlerForMsgType(actionType + 'ActionResult'), - actionFeedback: msgUtils.getHandlerForMsgType(actionType + 'ActionFeedback') - }; + this._options = options; this._pubSeqs = { result: 0, @@ -76,6 +48,22 @@ class ActionServer extends EventEmitter { this._statusListTimeout = 5; } + start() { + this._asInterface = new ActionServerInterface(this._options); + + this._asInterface.on('goal', this._handleGoal.bind(this)); + this._asInterface.on('cancel', this._handleCancel.bind(this)); + + const actionType = this._asInterface.getType(); + + this._messageTypes = { + result: ActionConfig.get().messages.getMessage(actionType + 'Result'), + feedback: ActionConfig.get().messages.getMessage(actionType + 'Feedback'), + actionResult: ActionConfig.get().messages.getMessage(actionType + 'ActionResult'), + actionFeedback: ActionConfig.get().messages.getMessage(actionType + 'ActionFeedback') + }; + } + generateGoalId() { return this._asInterface.generateGoalId(); } @@ -94,9 +82,9 @@ class ActionServer extends EventEmitter { let handle = this._getGoalHandle(newGoalId); if (handle) { - if (handle.status === GoalStatuses.RECALLING) { - handle.status = GoalStatuses.RECALLED; - this.publishResult(status.status, this._createMessage('result')); + // check if we already received a request to cancel this goal + if (handle.getStatusId() === GoalStatuses.RECALLING) { + handle.setCancelled(this._createMessage('result')); } handle._destructionTime = msg.goal_id.stamp; @@ -134,7 +122,7 @@ class ActionServer extends EventEmitter { for (let i = 0, len = this._goalHandleList.length; i < len; ++i) { const handle = this._goalHandleList[i]; const handleId = handle.id; - const handleStamp = handle.status.goal_id.stamp; + const handleStamp = handle.getStatus().goal_id.stamp; if (shouldCancelEverything || cancelId === handleId || @@ -226,3 +214,23 @@ class ActionServer extends EventEmitter { } module.exports = ActionServer; + + +//------------------------------------------------------------------------ +// Hook into configuration +//------------------------------------------------------------------------ + +let GoalStatusMsg, GoalStatuses, GoalIdMsg, GoalStatusArrayMsg; +let timeUtils, log, ActionServerInterface; + +ActionConfig.on('configured', function(config) { + timeUtils = config.time; + log = config.log; + + GoalStatusMsg = config.messages.getMessage('actionlib_msgs/GoalStatus'); + GoalStatuses = config.messages.getMessageConstants('actionlib_msgs/GoalStatus'); + GoalIdMsg = config.messages.getMessage('actionlib_msgs/GoalId'); + GoalStatusArrayMsg = config.messages.getMessage('actionlib_msgs/GoalStatusArray'); + + ActionServerInterface = config.ActionServerInterface; +}); diff --git a/actions/GoalHandle.js b/lib/GoalHandle.js similarity index 87% rename from actions/GoalHandle.js rename to lib/GoalHandle.js index c126ced..cd8dc29 100644 --- a/actions/GoalHandle.js +++ b/lib/GoalHandle.js @@ -17,15 +17,10 @@ 'use strict'; -const msgUtils = require('../utils/message_utils.js'); -const timeUtils = require('../lib/Time.js'); -const log = require('../lib/Logging.js').getLogger('ros.rosnodejs'); - -let GoalStatus = null; -let GoalStatuses = null; +const ActionConfig = require('./ActionConfig.js'); class GoalHandle { - constructor(goalId, actionServer, status) { + constructor(goalId, actionServer, status, goal) { if (goalId.id === '') { goalId = actionServer.generateGoalId(); } @@ -38,27 +33,18 @@ class GoalHandle { this._as = actionServer; - if (GoalStatus === null) { - GoalStatus = msgUtils.requireMsgPackage('actionlib_msgs').msg.GoalStatus; - GoalStatuses = GoalStatus.Constants; - } - this._status = new GoalStatus({ status: status || GoalStatuses.PENDING, goal_id: goalId }); + this._goal = goal; + this._destructionTime = timeUtils.epoch(); } - _isTerminalState() { - return [ - GoalStatuses.REJECTED, - GoalStatuses.RECALLED, - GoalStatuses.PREEMPTED, - GoalStatuses.ABORTED, - GoalStatuses.SUCCEEDED - ].includes(this._status.status); + getGoal() { + return this._goal; } getStatusId() { @@ -190,6 +176,29 @@ class GoalHandle { _logInvalidTransition(transition, currentStatus) { log.warn('Unable to %s from status %s for goal %s', transition, currentStatus, this.id); } + + _isTerminalState() { + return [ + GoalStatuses.REJECTED, + GoalStatuses.RECALLED, + GoalStatuses.PREEMPTED, + GoalStatuses.ABORTED, + GoalStatuses.SUCCEEDED + ].includes(this._status.status); + } } module.exports = GoalHandle; + +//------------------------------------------------------------------------ +// Hook into configuration +//------------------------------------------------------------------------ + +let timeUtils, log, GoalStatus, GoalStatuses; + +ActionConfig.on('configured', function(config) { + timeUtils = config.time; + log = config.log; + GoalStatus = config.messages.getMessage('actionlib_msgs/GoalStatus'); + GoalStatuses = config.messages.getMessageConstants('actionlib_msgs/GoalStatus'); +}); diff --git a/package.json b/package.json index 2a3f1b2..22bedc6 100644 --- a/package.json +++ b/package.json @@ -7,5 +7,10 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "chris smith", - "license": "Apache-2.0" + "license": "Apache-2.0", + "devDependencies": { + "chai": "^4.1.2", + "mocha": "^5.0.4", + "rosnodejs": "^2.2.0" + } } diff --git a/test/actionServerTest.js b/test/actionServerTest.js new file mode 100644 index 0000000..28d992a --- /dev/null +++ b/test/actionServerTest.js @@ -0,0 +1,29 @@ +'use strict' + +const net = require('net'); +const chai = require('chai'); +const expect = chai.expect; + +const rosnodejs = require('rosnodejs'); + +const ActionLib = require('../index.js'); + +ActionLib.config({ + time: rosnodejs.Time, + log: rosnodejs.log.getLogger('actionlibjs'), + messages: { + getMessage(fullName) { + const [pkg, name] = fullName.split('/'); + return rosnodejs.require(pkg).msg[name] + }, + getMessageConstants(fullName) { + return this.getMessage(fullName).CONSTANTS; + } + }, + ActionServerInterface, + ActionClientInterface +}); + +describe('action server', function() { + +});