Source: lib/action/server.js

  1. // Copyright (c) 2020 Matt Richard. All rights reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. 'use strict';
  15. const rclnodejs = require('bindings')('rclnodejs');
  16. const ActionInterfaces = require('./interfaces.js');
  17. const ActionUuid = require('./uuid.js');
  18. const Entity = require('../entity.js');
  19. const { CancelResponse, GoalEvent, GoalResponse } = require('./response.js');
  20. const loader = require('../interface_loader.js');
  21. const QoS = require('../qos.js');
  22. const ServerGoalHandle = require('./server_goal_handle.js');
  23. /**
  24. * Execute the goal.
  25. * @param {ServerGoalHandle} goalHandle - The server goal handle.
  26. * @returns {undefined}
  27. */
  28. function defaultHandleAcceptedCallback(goalHandle) {
  29. goalHandle.execute();
  30. }
  31. /**
  32. * Accept all goals.
  33. * @returns {number} - Always responds with acceptance.
  34. */
  35. function defaultGoalCallback() {
  36. return GoalResponse.ACCEPT;
  37. }
  38. /**
  39. * No cancellations.
  40. * @returns {number} - Always responds with rejection
  41. */
  42. function defaultCancelCallback() {
  43. return CancelResponse.REJECT;
  44. }
  45. /**
  46. * @class - ROS Action server.
  47. */
  48. class ActionServer extends Entity {
  49. /**
  50. * Creates a new action server.
  51. * @constructor
  52. *
  53. * @param {Node} node - The ROS node to add the action server to.
  54. * @param {function|string|object} typeClass - Type of the action.
  55. * @param {string} actionName - Name of the action. Used as part of the underlying topic and service names.
  56. * @param {function} executeCallback - Callback function for processing accepted goals.
  57. * @param {function} goalCallback - Callback function for handling new goal requests.
  58. * @param {function} handleAcceptedCallback - Callback function for handling newly accepted goals.
  59. * @param {function} cancelCallback - Callback function for handling cancel requests.
  60. * @param {object} options - Action server options.
  61. * @param {number} options.resultTimeout - How long in seconds a result is kept by the server after a goal reaches a terminal state in seconds, default: 900.
  62. * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
  63. * @param {object} options.qos - ROS Middleware "quality of service" options.
  64. * @param {QoS} options.qos.goalServiceQosProfile - Quality of service option for the goal service, default: QoS.profileServicesDefault.
  65. * @param {QoS} options.qos.resultServiceQosProfile - Quality of service option for the result service, default: QoS.profileServicesDefault.
  66. * @param {QoS} options.qos.cancelServiceQosProfile - Quality of service option for the cancel service, default: QoS.profileServicesDefault..
  67. * @param {QoS} options.qos.feedbackSubQosProfile - Quality of service option for the feedback subscription,
  68. * default: new QoS(QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT, 10).
  69. * @param {QoS} options.qos.statusSubQosProfile - Quality of service option for the status subscription, default: QoS.profileActionStatusDefault.
  70. */
  71. constructor(
  72. node,
  73. typeClass,
  74. actionName,
  75. executeCallback,
  76. goalCallback = defaultGoalCallback,
  77. handleAcceptedCallback = defaultHandleAcceptedCallback,
  78. cancelCallback = defaultCancelCallback,
  79. options
  80. ) {
  81. super(null, null, options);
  82. this._node = node;
  83. this._typeClass = loader.loadInterface(typeClass);
  84. this._actionName = actionName;
  85. this._goalHandles = new Map();
  86. // Setup options defaults
  87. this._options = this._options || {};
  88. this._options.resultTimeout =
  89. this._options.resultTimeout != null ? this._options.resultTimeout : 900;
  90. this._options.enableTypedArray = this._options.enableTypedArray !== false;
  91. this._options.qos = this._options.qos || {};
  92. this._options.qos.goalServiceQosProfile =
  93. this._options.qos.goalServiceQosProfile || QoS.profileServicesDefault;
  94. this._options.qos.resultServiceQosProfile =
  95. this._options.qos.resultServiceQosProfile || QoS.profileServicesDefault;
  96. this._options.qos.cancelServiceQosProfile =
  97. this._options.qos.cancelServiceQosProfile || QoS.profileServicesDefault;
  98. this._options.qos.feedbackSubQosProfile =
  99. this._options.qos.feedbackSubQosProfile ||
  100. new QoS(QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT, 10);
  101. this._options.qos.statusSubQosProfile =
  102. this._options.qos.statusSubQosProfile || QoS.profileActionStatusDefault;
  103. this.registerExecuteCallback(executeCallback);
  104. this.registerGoalCallback(goalCallback);
  105. this.registerHandleAcceptedCallback(handleAcceptedCallback);
  106. this.registerCancelCallback(cancelCallback);
  107. let type = this.typeClass.type();
  108. this._handle = rclnodejs.actionCreateServer(
  109. node.handle,
  110. node.getClock().handle,
  111. actionName,
  112. type.interfaceName,
  113. type.pkgName,
  114. this.qos.goalServiceQosProfile,
  115. this.qos.resultServiceQosProfile,
  116. this.qos.cancelServiceQosProfile,
  117. this.qos.feedbackSubQosProfile,
  118. this.qos.statusSubQosProfile,
  119. this.options.resultTimeout
  120. );
  121. node._addActionServer(this);
  122. }
  123. processGoalRequest(header, request) {
  124. this._executeGoalRequest(header, request);
  125. }
  126. processCancelRequest(header, request) {
  127. this._executeCancelRequest(header, request);
  128. }
  129. processResultRequest(header, request) {
  130. this._executeGetResultRequest(header, request);
  131. }
  132. processGoalExpired(result, count) {
  133. this._executeExpiredGoals(result, count);
  134. }
  135. /**
  136. * Register a callback for handling newly accepted goals.
  137. *
  138. * The provided function is called whenever a new goal has been accepted by this action server.
  139. * The function should expect an instance of {@link ServerGoalHandle} as an argument,
  140. * which represents a handle to the goal that was accepted.
  141. * The goal handle can be used to interact with the goal, e.g. publish feedback,
  142. * update the status, or execute a deferred goal.
  143. *
  144. * There can only be one handle accepted callback per {@link ActionServer},
  145. * therefore calling this function will replace any previously registered callback.
  146. *
  147. * @param {function} handleAcceptedCallback - Callback function, if not provided,
  148. * then unregisters any previously registered callback.
  149. * @returns {undefined}
  150. */
  151. registerHandleAcceptedCallback(handleAcceptedCallback) {
  152. this._handleAcceptedCallback =
  153. handleAcceptedCallback || defaultHandleAcceptedCallback;
  154. }
  155. /**
  156. * Register a callback for handling new goal requests.
  157. *
  158. * The purpose of the goal callback is to decide if a new goal should be accepted or rejected.
  159. * The callback should take the goal request message as a parameter and must return a {@link GoalResponse} value.
  160. *
  161. * @param {function} goalCallback - Callback function, if not provided,
  162. * then unregisters any previously registered callback.
  163. * @returns {undefined}
  164. */
  165. registerGoalCallback(goalCallback) {
  166. this._goalCallback = goalCallback || defaultGoalCallback;
  167. }
  168. /**
  169. * Register a callback for handling cancel requests.
  170. *
  171. * The purpose of the cancel callback is to decide if a request to cancel an on-going
  172. * (or queued) goal should be accepted or rejected.
  173. * The callback should take one parameter containing the cancel request (a goal handle) and must return a
  174. * {@link CancelResponse} value.
  175. *
  176. * There can only be one cancel callback per {@link ActionServer}, therefore calling this
  177. * function will replace any previously registered callback.
  178. * @param {function} cancelCallback - Callback function, if not provided,
  179. * then unregisters any previously registered callback.
  180. * @returns {undefined}
  181. */
  182. registerCancelCallback(cancelCallback) {
  183. this._cancelCallback = cancelCallback || defaultCancelCallback;
  184. }
  185. /**
  186. * Register a callback for executing action goals.
  187. *
  188. * The purpose of the execute callback is to execute the action goal and return a result when finished.
  189. * The callback should take one parameter containing goal request and must return a
  190. * result instance (i.e. `action_type.Result`).
  191. *
  192. * There can only be one execute callback per {@link ActionServer}, therefore calling this
  193. * function will replace any previously registered callback.
  194. *
  195. * @param {function} executeCallback - Callback function.
  196. * @returns {undefined}
  197. */
  198. registerExecuteCallback(executeCallback) {
  199. if (typeof executeCallback !== 'function') {
  200. throw new TypeError('Invalid argument');
  201. }
  202. this._callback = executeCallback;
  203. }
  204. notifyExecute(goalHandle, callback) {
  205. if (!callback && !this._callback) {
  206. return;
  207. }
  208. this._executeGoal(callback || this._callback, goalHandle);
  209. }
  210. _sendResultResponse(header, result) {
  211. rclnodejs.actionSendResultResponse(this.handle, header, result.serialize());
  212. }
  213. _executeGoalRequest(header, request) {
  214. let goalUuid = request.goal_id;
  215. let goalInfo = new ActionInterfaces.GoalInfo();
  216. goalInfo['goal_id'] = goalUuid;
  217. // Initialize the stamp (otherwise we will get an error when we serialize the message)
  218. goalInfo.stamp = {
  219. sec: 0,
  220. nanosec: 0,
  221. };
  222. this._node.getLogger().debug(`New goal request with ID: ${goalUuid.uuid}`);
  223. let goalIdExists = rclnodejs.actionServerGoalExists(
  224. this._handle,
  225. goalInfo.serialize()
  226. );
  227. let accepted = false;
  228. if (!goalIdExists) {
  229. let result = this._goalCallback(
  230. request.goal.toPlainObject(this.typedArrayEnabled)
  231. );
  232. accepted = result === GoalResponse.ACCEPT;
  233. }
  234. let goalHandle;
  235. if (accepted) {
  236. // Stamp time of acceptance
  237. const secondsAndNanos = this._node.getClock().now().secondsAndNanoseconds;
  238. goalInfo.stamp = {
  239. sec: secondsAndNanos.seconds,
  240. nanosec: secondsAndNanos.nanoseconds,
  241. };
  242. try {
  243. goalHandle = new ServerGoalHandle(this, goalInfo, request.goal);
  244. } catch (error) {
  245. this._node
  246. .getLogger()
  247. .error(
  248. `Failed to accept new goal with ID ${goalUuid.uuid}: ${error}`
  249. );
  250. accepted = false;
  251. }
  252. if (accepted) {
  253. let uuid = ActionUuid.fromBytes(goalUuid.uuid).toString();
  254. this._goalHandles.set(uuid, goalHandle);
  255. }
  256. }
  257. // Send response
  258. let response = new this.typeClass.impl.SendGoalService.Response();
  259. response.accepted = accepted;
  260. response.stamp = goalInfo.stamp;
  261. rclnodejs.actionSendGoalResponse(
  262. this._handle,
  263. header,
  264. response.serialize()
  265. );
  266. if (!accepted) {
  267. this._node.getLogger().debug(`New goal rejected: ${goalUuid.uuid}`);
  268. return;
  269. }
  270. this._node.getLogger().debug(`New goal accepted: ${goalUuid.uuid}`);
  271. this._handleAcceptedCallback(goalHandle);
  272. }
  273. async _executeCancelRequest(header, request) {
  274. let Response = this.typeClass.impl.CancelGoal.Response;
  275. let response = new Response();
  276. this._node.getLogger().debug(`Cancel request received: ${request}`);
  277. // Get list of goals that are requested to be canceled
  278. const msgHandle = rclnodejs.actionProcessCancelRequest(
  279. this.handle,
  280. request.serialize(),
  281. response.toRawROS()
  282. );
  283. let cancelResponse = new Response();
  284. cancelResponse.deserialize(response.refObject);
  285. let goalsCanceling = [];
  286. for (let goalInfo of cancelResponse.goals_canceling.data) {
  287. let uuid = ActionUuid.fromBytes(goalInfo.goal_id.uuid).toString();
  288. // Possibly the user doesn't care to track the goal handle
  289. if (this._goalHandles.has(uuid)) {
  290. let goalHandle = this._goalHandles.get(uuid);
  291. let result = await this._cancelCallback(goalHandle);
  292. if (result === CancelResponse.ACCEPT) {
  293. // Notify goal handle
  294. goalHandle._updateState(GoalEvent.CANCEL_GOAL);
  295. goalsCanceling.push(goalInfo);
  296. }
  297. }
  298. }
  299. cancelResponse['goals_canceling'] = goalsCanceling;
  300. rclnodejs.actionSendCancelResponse(
  301. this.handle,
  302. header,
  303. cancelResponse.serialize()
  304. );
  305. msgHandle.release();
  306. }
  307. async _executeGoal(callback, goalHandle) {
  308. const goalUuid = goalHandle.goalId.uuid;
  309. this._node.getLogger().debug(`Executing goal with ID ${goalUuid}`);
  310. let result;
  311. try {
  312. result = await callback(goalHandle);
  313. } catch (error) {
  314. // Create an empty result so that we can still send a response to the client
  315. result = new this.typeClass.Result();
  316. this._node
  317. .getLogger()
  318. .error(`Error raised in execute callback: ${error}`);
  319. }
  320. // If user did not trigger a terminal state, assume aborted
  321. if (goalHandle.isActive) {
  322. this._node
  323. .getLogger()
  324. .warn(`Goal state not set, assuming aborted. Goal ID: ${goalUuid}`);
  325. goalHandle.abort();
  326. }
  327. this._node
  328. .getLogger()
  329. .debug(
  330. `Goal with ID ${goalUuid} finished with state ${goalHandle.status}`
  331. );
  332. // Set result
  333. let response = new this.typeClass.impl.GetResultService.Response();
  334. response.status = goalHandle.status;
  335. response.result = result;
  336. goalHandle._deferred.setResult(response);
  337. }
  338. _executeGetResultRequest(header, request) {
  339. let uuid = ActionUuid.fromBytes(request.goal_id.uuid).toString();
  340. this._node
  341. .getLogger()
  342. .debug(
  343. `Result request received for goal with ID: ${request.goal_id.uuid}`
  344. );
  345. // If no goal with the requested ID exists, then return UNKNOWN status
  346. if (!this._goalHandles.has(uuid)) {
  347. this._node
  348. .getLogger()
  349. .debug(
  350. `Sending result response for unknown goal ID: ${request.goal_id.uuid}`
  351. );
  352. let response = new this.typeClass.impl.GetResultService.Response();
  353. response.status = ActionInterfaces.GoalStatus.STATUS_UNKNOWN;
  354. rclnodejs.actionSendResultResponse(
  355. this.handle,
  356. header,
  357. response.serialize()
  358. );
  359. return;
  360. }
  361. this._goalHandles
  362. .get(uuid)
  363. ._deferred.setDoneCallback((result) =>
  364. this._sendResultResponse(header, result)
  365. );
  366. }
  367. _executeExpiredGoals(result, count) {
  368. for (let i = 0; i < count; i++) {
  369. const goal = result.data[i];
  370. const goalInfo = new ActionInterfaces.GoalInfo();
  371. goalInfo.deserialize(goal.refObject);
  372. let uuid = ActionUuid.fromBytes(goalInfo.goal_id.uuid).toString();
  373. this._goalHandles.delete(uuid);
  374. }
  375. }
  376. /**
  377. * Destroy the action server and all goals.
  378. * @return {undefined}
  379. */
  380. destroy() {
  381. if (this._destroyed) {
  382. return;
  383. }
  384. for (let goalHandle of Array.from(this._goalHandles.values())) {
  385. goalHandle.destroy();
  386. }
  387. this._goalHandles.clear();
  388. this._node._destroyEntity(this, this._node._actionServers);
  389. this._destroyed = true;
  390. }
  391. }
  392. module.exports = ActionServer;