Skip to content

Commit 9066454

Browse files
authored
Improved Action Support (#110)
* Updates to base ActionServer and new ActionClient * Adds Simplified action server/client wrappers * Updates to ActionClientInterface to support new ActionClient * Adds example using SimpleActionServer/Client
1 parent 463a17e commit 9066454

14 files changed

Lines changed: 1584 additions & 121 deletions

src/actions/ActionClient.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2017 Rethink Robotics
3+
*
4+
* Copyright 2017 Chris Smith
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
'use strict';
19+
20+
const msgUtils = require('../utils/message_utils.js');
21+
22+
const ActionClientInterface = require('../lib/ActionClientInterface.js');
23+
24+
const EventEmitter = require('events');
25+
26+
const ClientGoalHandle = require('./ClientGoalHandle.js');
27+
const Time = require('../lib/Time.js');
28+
29+
const log = require('../lib/Logging.js').getLogger('actionlib_nodejs');
30+
const ThisNode = require('../lib/ThisNode.js');
31+
32+
let GOAL_COUNT = 0;
33+
34+
/**
35+
* @class ActionClient
36+
* EXPERIMENTAL
37+
*
38+
*/
39+
class ActionClient extends EventEmitter {
40+
constructor(options) {
41+
super();
42+
43+
this._acInterface = new ActionClientInterface(options);
44+
45+
this._acInterface.on('status', this._handleStatus.bind(this));
46+
this._acInterface.on('feedback', this._handleFeedback.bind(this));
47+
this._acInterface.on('result', this._handleResult.bind(this));
48+
49+
const actionType = this._acInterface.getType();
50+
this._messageTypes = this._messageTypes = {
51+
result: msgUtils.getHandlerForMsgType(actionType + 'Result'),
52+
feedback: msgUtils.getHandlerForMsgType(actionType + 'Feedback'),
53+
goal: msgUtils.getHandlerForMsgType(actionType + 'Goal'),
54+
actionResult: msgUtils.getHandlerForMsgType(actionType + 'ActionResult'),
55+
actionFeedback: msgUtils.getHandlerForMsgType(actionType + 'ActionFeedback'),
56+
actionGoal: msgUtils.getHandlerForMsgType(actionType + 'ActionGoal')
57+
};
58+
59+
this._goalLookup = {};
60+
}
61+
62+
shutdown() {
63+
return this._acInterface.shutdown();
64+
}
65+
66+
sendGoal(goal, transitionCb = null, feedbackCb = null) {
67+
const actionGoal = new this._messageTypes.actionGoal();
68+
69+
const now = Time.now();
70+
actionGoal.header.stamp = now;
71+
actionGoal.goal_id.stamp = now;
72+
const goalIdStr = `${ThisNode.getNodeName()}-${GOAL_COUNT++}-${now.secs}.${now.nsecs}`;
73+
actionGoal.goal_id.id = goalIdStr;
74+
actionGoal.goal = goal;
75+
76+
this._acInterface.sendGoal(actionGoal);
77+
78+
const handle = new ClientGoalHandle(actionGoal, this._acInterface);
79+
80+
if (transitionCb && typeof transitionCb === 'function') {
81+
handle.on('transition', transitionCb);
82+
}
83+
if (feedbackCb && typeof feedbackCb === 'function') {
84+
handle.on('feedback', feedbackCb);
85+
}
86+
87+
this._goalLookup[goalIdStr] = handle;
88+
89+
return handle;
90+
}
91+
92+
cancelAllGoals() {
93+
this._acInterface.cancel("", { secs: 0, nsecs: 0});
94+
}
95+
96+
cancelGoalsAtAndBeforeTime(stamp) {
97+
this._acInterface.cancel("", stamp);
98+
}
99+
100+
waitForActionServerToStart(timeout) {
101+
return this._acInterface.waitForActionServerToStart(timeout);
102+
}
103+
104+
isServerConnected() {
105+
return this._acInterface.isServerConnected();
106+
}
107+
108+
_handleStatus(msg) {
109+
const list = msg.status_list;
110+
const len = list.length;
111+
112+
const statusMap = {};
113+
114+
for (let i = 0; i < len; ++i) {
115+
const entry = list[i];
116+
const goalId = entry.goal_id.id;
117+
118+
statusMap[goalId] = entry;
119+
}
120+
121+
for (let goalId in this._goalLookup) {
122+
const goalHandle = this._goalLookup[goalId];
123+
goalHandle.updateStatus(statusMap[goalId]);
124+
}
125+
}
126+
127+
_handleFeedback(msg) {
128+
const goalId = msg.status.goal_id.id;
129+
const goalHandle = this._goalLookup[goalId];
130+
if (goalHandle) {
131+
goalHandle.updateFeedback(msg);
132+
}
133+
}
134+
135+
_handleResult(msg) {
136+
const goalId = msg.status.goal_id.id;
137+
const goalHandle = this._goalLookup[goalId];
138+
if (goalHandle) {
139+
delete this._goalLookup[goalId];
140+
goalHandle.updateResult(msg);
141+
}
142+
}
143+
}
144+
145+
module.exports = ActionClient;

src/actions/ActionServer.js

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717

1818
'use strict';
1919

20-
const timeUtils = require('../lib/Time.js');
2120
const msgUtils = require('../utils/message_utils.js');
2221
const EventEmitter = require('events');
22+
const Ultron = require('ultron');
2323

2424
const ActionServerInterface = require('../lib/ActionServerInterface.js');
2525
const GoalHandle = require('./GoalHandle.js');
2626
const Time = require('../lib/Time.js');
2727

28+
const log = require('../lib/Logging.js').getLogger('actionlib_nodejs');
29+
const ThisNode = require('../lib/ThisNode.js');
30+
2831
let GoalIdMsg = null;
2932
let GoalStatusMsg = null;
3033
let GoalStatusArrayMsg = null;
@@ -49,7 +52,27 @@ class ActionServer extends EventEmitter {
4952
GoalStatusArrayMsg = msgUtils.requireMsgPackage('actionlib_msgs').msg.GoalStatusArray;
5053
}
5154

52-
this._asInterface = new ActionServerInterface(options);
55+
this._options = options;
56+
57+
this._pubSeqs = {
58+
result: 0,
59+
feedback: 0,
60+
status: 0
61+
};
62+
63+
this._goalHandleList = [];
64+
this._goalHandleCache = {};
65+
66+
this._lastCancelStamp = Time.epoch();
67+
68+
this._statusListTimeout = { secs: 5, nsecs: 0 };
69+
this._shutdown = false;
70+
this._ultron = new Ultron(ThisNode);
71+
}
72+
73+
start() {
74+
this._started = true;
75+
this._asInterface = new ActionServerInterface(this._options);
5376

5477
this._asInterface.on('goal', this._handleGoal.bind(this));
5578
this._asInterface.on('cancel', this._handleCancel.bind(this));
@@ -63,36 +86,50 @@ class ActionServer extends EventEmitter {
6386
actionFeedback: msgUtils.getHandlerForMsgType(actionType + 'ActionFeedback')
6487
};
6588

66-
this._pubSeqs = {
67-
result: 0,
68-
feedback: 0,
69-
status: 0
70-
};
89+
this.publishStatus();
7190

72-
this._goalHandleList = [];
73-
this._goalHandleCache = {};
91+
let statusFreq = 5;
92+
if (this._options.statusFrequency !== undefined) {
93+
if (typeof this._options.statusFrequency !== 'number') {
94+
throw new Error(`Invalid value (${this._options.statusFrequency}) for statusFrequency - expected number`);
95+
}
7496

75-
this._lastCancelStamp = timeUtils.epoch();
97+
statusFreq = this._options.statusFrequency;
98+
}
7699

77-
this._statusFrequency = 5;
78-
this._statusListTimeout = 5;
79-
this._statusHandle = null;
100+
if (statusFreq > 0) {
101+
this._statusFreqInt = setInterval(() => {
102+
this.publishStatus();
103+
}, 1000 / statusFreq);
104+
}
80105

81-
this._started = false;
106+
// FIXME: how to handle shutdown? Should user be responsible?
107+
// should we check for shutdown in interval instead of listening
108+
// to events here?
109+
this._ultron.once('shutdown', () => { this.shutdown(); });
82110
}
83111

84112
generateGoalId() {
85113
return this._asInterface.generateGoalId();
86114
}
87115

88-
start() {
89-
this._started = true;
90-
this._statusHandle = setInterval(this.publishStatus.bind(this), 1000 / this._statusFrequency);
91-
}
92-
93116
shutdown() {
94-
clearInterval(this._statusHandle);
95-
return this._asInterface.shutdown();
117+
if (!this._shutdown) {
118+
this._shutdown = true;
119+
this.removeAllListeners();
120+
121+
clearInterval(this._statusFreqInt);
122+
this._statusFreqInt = null;
123+
124+
this._ultron.destroy();
125+
this._ultron = null;
126+
127+
if (this._asInterface) {
128+
return this._asInterface.shutdown();
129+
}
130+
}
131+
// else
132+
return Promise.resolve();
96133
}
97134

98135
_getGoalHandle(id) {
@@ -109,9 +146,9 @@ class ActionServer extends EventEmitter {
109146
let handle = this._getGoalHandle(newGoalId);
110147

111148
if (handle) {
112-
if (handle._status.status === GoalStatuses.RECALLING) {
113-
handle._status.status = GoalStatuses.RECALLED;
114-
this.publishResult(handle._status, this._createMessage('result'));
149+
// check if we already received a request to cancel this goal
150+
if (handle.getStatusId() === GoalStatuses.RECALLING) {
151+
handle.setCancelled(this._createMessage('result'));
115152
}
116153

117154
handle._destructionTime = msg.goal_id.stamp;
@@ -124,8 +161,8 @@ class ActionServer extends EventEmitter {
124161

125162
const goalStamp = msg.goal_id.stamp;
126163
// check if this goal has already been cancelled based on its timestamp
127-
if (!timeUtils.isZeroTime(goalStamp) &&
128-
timeUtils.timeComp(goalStamp, this._lastCancelStamp) < 0) {
164+
if (!Time.isZeroTime(goalStamp) &&
165+
Time.timeComp(goalStamp, this._lastCancelStamp) < 0) {
129166
handle.setCancelled(this._createMessage('result'));
130167
return false;
131168
}
@@ -144,7 +181,7 @@ class ActionServer extends EventEmitter {
144181

145182
const cancelId = msg.id;
146183
const cancelStamp = msg.stamp;
147-
const cancelStampIsZero = timeUtils.isZeroTime(cancelStamp);
184+
const cancelStampIsZero = Time.isZeroTime(cancelStamp);
148185

149186
const shouldCancelEverything = (cancelId === '' && cancelStampIsZero);
150187

@@ -153,12 +190,12 @@ class ActionServer extends EventEmitter {
153190
for (let i = 0, len = this._goalHandleList.length; i < len; ++i) {
154191
const handle = this._goalHandleList[i];
155192
const handleId = handle.id;
156-
const handleStamp = handle._status.goal_id.stamp;
193+
const handleStamp = handle.getStatus().goal_id.stamp;
157194

158195
if (shouldCancelEverything ||
159196
cancelId === handleId ||
160-
(!timeUtils.isZeroTime(handleStamp) &&
161-
timeUtils.timeComp(handleStamp, cancelStamp) < 0))
197+
(!Time.isZeroTime(handleStamp) &&
198+
Time.timeComp(handleStamp, cancelStamp) < 0))
162199
{
163200
if (cancelId === handleId) {
164201
goalIdFound = true;
@@ -179,7 +216,7 @@ class ActionServer extends EventEmitter {
179216
}
180217

181218
// update the last cancel stamp if new one occurred later
182-
if (timeUtils.timeComp(cancelStamp, this._lastCancelStamp) > 0) {
219+
if (Time.timeComp(cancelStamp, this._lastCancelStamp) > 0) {
183220
this._lastCancelStamp = cancelStamp;
184221
}
185222
}
@@ -207,16 +244,15 @@ class ActionServer extends EventEmitter {
207244

208245
let goalsToRemove = new Set();
209246

210-
const now = timeUtils.toNumber(Time.now());
247+
const now = Time.now();
211248

212249
for (let i = 0, len = this._goalHandleList.length; i < len; ++i) {
213250
const goalHandle = this._goalHandleList[i];
214251
msg.status_list.push(goalHandle.getGoalStatus());
215252

216253
const t = goalHandle._destructionTime;
217-
const tNum = timeUtils.toNumber(t);
218-
if (!timeUtils.isZeroTime(t) &&
219-
timeUtils.toNumber(t) + this._statusListTimeout < now)
254+
if (!Time.isZeroTime(t) &&
255+
Time.lt(Time.add(t, this._statusListTimeout), now))
220256
{
221257
goalsToRemove.add(goalHandle);
222258
}

0 commit comments

Comments
 (0)