You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

action.py 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. # Copyright (c) 2016-2017 Anki, Inc.
  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 in the file LICENSE.txt or 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. '''
  15. Actions encapsulate specific high-level tasks that the Cozmo robot can perform.
  16. They have a definite beginning and end.
  17. These tasks include picking up an object, rotating in place, saying text, etc.
  18. Actions are usually triggered by a call to a method on the
  19. :class:`cozmo.robot.Robot` class such as :meth:`~cozmo.robot.Robot.turn_in_place`
  20. The call will return an object that subclasses :class:`Action` that can be
  21. used to cancel the action, or be observed to wait or be notified when the
  22. action completes (or fails) by calling its
  23. :meth:`~cozmo.event.Dispatcher.wait_for` or
  24. :meth:`~cozmo.event.Dispatcher.add_event_handler` methods.
  25. Warning:
  26. Unless you pass ``in_parallel=True`` when starting the action, no other
  27. action can be active at the same time. Attempting to trigger a non-parallel
  28. action when another action is already in progress will result in a
  29. :class:`~cozmo.exceptions.RobotBusy` exception being raised.
  30. When using ``in_parallel=True`` you may see an action fail with the result
  31. :attr:`ActionResults.TRACKS_LOCKED` - this indicates that another in-progress
  32. action has already locked that movement track (e.g. two actions cannot
  33. move the head at the same time).
  34. '''
  35. # __all__ should order by constants, event classes, other classes, functions.
  36. __all__ = ['ACTION_IDLE', 'ACTION_RUNNING', 'ACTION_SUCCEEDED',
  37. 'ACTION_FAILED', 'ACTION_ABORTING',
  38. 'EvtActionStarted', 'EvtActionCompleted', 'Action', 'ActionResults']
  39. from collections import namedtuple
  40. import sys
  41. from . import logger
  42. from . import event
  43. from . import exceptions
  44. from ._clad import _clad_to_engine_iface, _clad_to_engine_cozmo, _clad_to_game_cozmo, CladEnumWrapper
  45. #: string: Action idle state
  46. ACTION_IDLE = 'action_idle'
  47. #: string: Action running state
  48. ACTION_RUNNING = 'action_running'
  49. #: string: Action succeeded state
  50. ACTION_SUCCEEDED = 'action_succeeded'
  51. #: string: Action failed state
  52. ACTION_FAILED = 'action_failed'
  53. #: string: Action failed state
  54. ACTION_ABORTING = 'action_aborting'
  55. _VALID_STATES = {ACTION_IDLE, ACTION_RUNNING, ACTION_SUCCEEDED, ACTION_FAILED, ACTION_ABORTING}
  56. class _ActionResult(namedtuple('_ActionResult', 'name id')):
  57. # Tuple mapping between CLAD ActionResult name and ID
  58. # All instances will be members of ActionResults
  59. # Keep _ActionResult as lightweight as a normal namedtuple
  60. __slots__ = ()
  61. def __str__(self):
  62. return 'ActionResults.%s' % self.name
  63. class ActionResults(CladEnumWrapper):
  64. """The possible result values for an Action.
  65. An Action's result is set when the action completes.
  66. """
  67. _clad_enum = _clad_to_game_cozmo.ActionResult
  68. _entry_type = _ActionResult
  69. #: Action completed successfully.
  70. SUCCESS = _ActionResult("SUCCESS", _clad_enum.SUCCESS)
  71. #: Action is still running.
  72. RUNNING = _ActionResult("RUNNING", _clad_enum.RUNNING)
  73. #: Action was cancelled (e.g. via :meth:`~cozmo.robot.Robot.abort_all_actions` or
  74. #: :meth:`Action.abort`).
  75. CANCELLED_WHILE_RUNNING = _ActionResult("CANCELLED_WHILE_RUNNING", _clad_enum.CANCELLED_WHILE_RUNNING)
  76. #: Action aborted itself (e.g. had invalid attributes, or a runtime failure).
  77. ABORT = _ActionResult("ABORT", _clad_enum.ABORT)
  78. #: Animation Action aborted itself (e.g. there was an error playing the animation).
  79. ANIM_ABORTED = _ActionResult("ANIM_ABORTED", _clad_enum.ANIM_ABORTED)
  80. #: There was an error related to vision markers.
  81. BAD_MARKER = _ActionResult("BAD_MARKER", _clad_enum.BAD_MARKER)
  82. # (Undocumented) There was a problem related to a subscribed or unsupported message tag (indicates bug in engine)
  83. BAD_MESSAGE_TAG = _ActionResult("BAD_MESSAGE_TAG", _clad_enum.BAD_MESSAGE_TAG)
  84. #: There was a problem with the Object ID provided (e.g. there is no Object with that ID).
  85. BAD_OBJECT = _ActionResult("BAD_OBJECT", _clad_enum.BAD_OBJECT)
  86. #: There was a problem with the Pose provided.
  87. BAD_POSE = _ActionResult("BAD_POSE", _clad_enum.BAD_POSE)
  88. # (Undocumented) The SDK-provided tag was bad (shouldn't occur - would indicate a bug in the SDK)
  89. BAD_TAG = _ActionResult("BAD_TAG", _clad_enum.BAD_TAG)
  90. # (Undocumented) Shouldn't occur outside of factory
  91. FAILED_SETTING_CALIBRATION = _ActionResult("FAILED_SETTING_CALIBRATION", _clad_enum.FAILED_SETTING_CALIBRATION)
  92. #: There was an error following the planned path.
  93. FOLLOWING_PATH_BUT_NOT_TRAVERSING = _ActionResult("FOLLOWING_PATH_BUT_NOT_TRAVERSING", _clad_enum.FOLLOWING_PATH_BUT_NOT_TRAVERSING)
  94. #: The action was interrupted by another Action or Behavior.
  95. INTERRUPTED = _ActionResult("INTERRUPTED", _clad_enum.INTERRUPTED)
  96. #: The robot ended up in an "off treads state" not valid for this action (e.g.
  97. #: the robot was placed on its back while executing a turn)
  98. INVALID_OFF_TREADS_STATE = _ActionResult("INVALID_OFF_TREADS_STATE",
  99. _clad_to_game_cozmo.ActionResult.INVALID_OFF_TREADS_STATE)
  100. #: The Up Axis of a carried object doesn't match the desired placement pose.
  101. MISMATCHED_UP_AXIS = _ActionResult("MISMATCHED_UP_AXIS", _clad_enum.MISMATCHED_UP_AXIS)
  102. #: No valid Animation name was found.
  103. NO_ANIM_NAME = _ActionResult("NO_ANIM_NAME", _clad_enum.NO_ANIM_NAME)
  104. #: An invalid distance value was given.
  105. NO_DISTANCE_SET = _ActionResult("NO_DISTANCE_SET", _clad_enum.NO_DISTANCE_SET)
  106. #: There was a problem with the Face ID (e.g. Cozmo doesn't no where it is).
  107. NO_FACE = _ActionResult("NO_FACE", _clad_enum.NO_FACE)
  108. #: No goal pose was set.
  109. NO_GOAL_SET = _ActionResult("NO_GOAL_SET", _clad_enum.NO_GOAL_SET)
  110. #: No pre-action poses were found (e.g. could not get into position).
  111. NO_PREACTION_POSES = _ActionResult("NO_PREACTION_POSES", _clad_enum.NO_PREACTION_POSES)
  112. #: No object is being carried, but the action requires one.
  113. NOT_CARRYING_OBJECT_ABORT = _ActionResult("NOT_CARRYING_OBJECT_ABORT", _clad_enum.NOT_CARRYING_OBJECT_ABORT)
  114. #: Initial state of an Action to indicate it has not yet started.
  115. NOT_STARTED = _ActionResult("NOT_STARTED", _clad_enum.NOT_STARTED)
  116. #: No sub-action was provided.
  117. NULL_SUBACTION = _ActionResult("NULL_SUBACTION", _clad_enum.NULL_SUBACTION)
  118. #: Cozmo was unable to plan a path.
  119. PATH_PLANNING_FAILED_ABORT = _ActionResult("PATH_PLANNING_FAILED_ABORT", _clad_enum.PATH_PLANNING_FAILED_ABORT)
  120. #: The object that Cozmo is attempting to pickup is unexpectedly moving (e.g
  121. #: it is being moved by someone else).
  122. PICKUP_OBJECT_UNEXPECTEDLY_MOVING = _ActionResult("PICKUP_OBJECT_UNEXPECTEDLY_MOVING", _clad_enum.PICKUP_OBJECT_UNEXPECTEDLY_MOVING)
  123. #: The object that Cozmo thought he was lifting didn't start moving, so he
  124. #: must have missed.
  125. PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING = _ActionResult("PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING", _clad_enum.PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING)
  126. # (Undocumented) Shouldn't occur in SDK usage
  127. SEND_MESSAGE_TO_ROBOT_FAILED = _ActionResult("SEND_MESSAGE_TO_ROBOT_FAILED", _clad_enum.SEND_MESSAGE_TO_ROBOT_FAILED)
  128. #: Cozmo is unexpectedly still carrying an object.
  129. STILL_CARRYING_OBJECT = _ActionResult("STILL_CARRYING_OBJECT", _clad_enum.STILL_CARRYING_OBJECT)
  130. #: The Action timed out before completing correctly.
  131. TIMEOUT = _ActionResult("TIMEOUT", _clad_enum.TIMEOUT)
  132. #: One or more animation tracks (Head, Lift, Body, Face, Backpack Lights, Audio)
  133. #: are already being used by another Action.
  134. TRACKS_LOCKED = _ActionResult("TRACKS_LOCKED", _clad_enum.TRACKS_LOCKED)
  135. #: There was an internal error related to an unexpected type of dock action.
  136. UNEXPECTED_DOCK_ACTION = _ActionResult("UNEXPECTED_DOCK_ACTION", _clad_enum.UNEXPECTED_DOCK_ACTION)
  137. # (Undocumented) Shouldn't occur outside of factory.
  138. UNKNOWN_TOOL_CODE = _ActionResult("UNKNOWN_TOOL_CODE", _clad_enum.UNKNOWN_TOOL_CODE)
  139. # (Undocumented) There was a problem in the subclass's update.
  140. UPDATE_DERIVED_FAILED = _ActionResult("UPDATE_DERIVED_FAILED", _clad_enum.UPDATE_DERIVED_FAILED)
  141. #: Cozmo did not see the expected result (e.g. unable to see cubes in their
  142. #: expected position after a related action).
  143. VISUAL_OBSERVATION_FAILED = _ActionResult("VISUAL_OBSERVATION_FAILED", _clad_enum.VISUAL_OBSERVATION_FAILED)
  144. #: The Action failed, but may succeed if retried.
  145. RETRY = _ActionResult("RETRY", _clad_enum.RETRY)
  146. #: Failed to get into position.
  147. DID_NOT_REACH_PREACTION_POSE = _ActionResult("DID_NOT_REACH_PREACTION_POSE", _clad_enum.DID_NOT_REACH_PREACTION_POSE)
  148. #: Failed to follow the planned path.
  149. FAILED_TRAVERSING_PATH = _ActionResult("FAILED_TRAVERSING_PATH", _clad_enum.FAILED_TRAVERSING_PATH)
  150. #: The previous attempt to pick and place an object failed.
  151. LAST_PICK_AND_PLACE_FAILED = _ActionResult("LAST_PICK_AND_PLACE_FAILED", _clad_enum.LAST_PICK_AND_PLACE_FAILED)
  152. #: The required motor isn't moving so the action cannot complete.
  153. MOTOR_STOPPED_MAKING_PROGRESS = _ActionResult("MOTOR_STOPPED_MAKING_PROGRESS", _clad_enum.MOTOR_STOPPED_MAKING_PROGRESS)
  154. #: Not carrying an object when it was expected, but may succeed if the action is retried.
  155. NOT_CARRYING_OBJECT_RETRY = _ActionResult("NOT_CARRYING_OBJECT_RETRY", _clad_enum.NOT_CARRYING_OBJECT_RETRY)
  156. #: Cozmo is expected to be on the charger, but is not.
  157. NOT_ON_CHARGER = _ActionResult("NOT_ON_CHARGER", _clad_enum.NOT_ON_CHARGER)
  158. #: Cozmo was unable to plan a path, but may succeed if the action is retried.
  159. PATH_PLANNING_FAILED_RETRY = _ActionResult("PATH_PLANNING_FAILED_RETRY", _clad_enum.PATH_PLANNING_FAILED_RETRY)
  160. #: There is no room to place the object at the desired destination.
  161. PLACEMENT_GOAL_NOT_FREE = _ActionResult("PLACEMENT_GOAL_NOT_FREE", _clad_enum.PLACEMENT_GOAL_NOT_FREE)
  162. #: Cozmo failed to drive off the charger.
  163. STILL_ON_CHARGER = _ActionResult("STILL_ON_CHARGER", _clad_enum.STILL_ON_CHARGER)
  164. #: Cozmo's pitch is at an unexpected angle for the Action.
  165. UNEXPECTED_PITCH_ANGLE = _ActionResult("UNEXPECTED_PITCH_ANGLE", _clad_enum.UNEXPECTED_PITCH_ANGLE)
  166. ActionResults._init_class()
  167. class EvtActionStarted(event.Event):
  168. '''Triggered when a robot starts an action.'''
  169. action = "The action that started"
  170. class EvtActionCompleted(event.Event):
  171. '''Triggered when a robot action has completed or failed.'''
  172. action = "The action that completed"
  173. state = 'The state of the action; either cozmo.action.ACTION_SUCCEEDED or cozmo.action.ACTION_FAILED'
  174. failure_code = 'A failure code such as "cancelled"'
  175. failure_reason = 'A human-readable failure reason'
  176. class Action(event.Dispatcher):
  177. """An action holds the state of an in-progress robot action
  178. """
  179. # We allow sub-classes of Action to optionally disable logging messages
  180. # related to those actions being aborted - this is useful for actions
  181. # that are aborted frequently (by design) and would otherwise spam the log
  182. _enable_abort_logging = True
  183. def __init__(self, *, conn, robot, **kw):
  184. super().__init__(**kw)
  185. #: :class:`~cozmo.conn.CozmoConnection`: The connection on which the action was sent.
  186. self.conn = conn
  187. #: :class:`~cozmo.robot.Robot`: Th robot instance executing the action.
  188. self.robot = robot
  189. self._action_id = None
  190. self._state = ACTION_IDLE
  191. self._failure_code = None
  192. self._failure_reason = None
  193. self._result = None
  194. self._completed_event = None
  195. self._completed_event_pending = False
  196. def __repr__(self):
  197. extra = self._repr_values()
  198. if len(extra) > 0:
  199. extra = ' '+extra
  200. if self._state == ACTION_FAILED:
  201. extra += (" failure_reason='%s' failure_code=%s result=%s" %
  202. (self._failure_reason, self._failure_code, self.result))
  203. return '<%s state=%s%s>' % (self.__class__.__name__, self.state, extra)
  204. def _repr_values(self):
  205. return ''
  206. def _encode(self):
  207. raise NotImplementedError()
  208. def _start(self):
  209. self._state = ACTION_RUNNING
  210. self.dispatch_event(EvtActionStarted, action=self)
  211. def _set_completed(self, msg):
  212. self._state = ACTION_SUCCEEDED
  213. self._completed_event_pending = False
  214. self._dispatch_completed_event(msg)
  215. def _dispatch_completed_event(self, msg):
  216. # Override to extra action-specific data from msg and generate
  217. # an action-specific completion event. Do not call super if overriden.
  218. # Must generate a subclass of EvtActionCompleted.
  219. self._completed_event = EvtActionCompleted(action=self, state=self._state)
  220. self.dispatch_event(self._completed_event)
  221. def _set_failed(self, code, reason):
  222. self._state = ACTION_FAILED
  223. self._failure_code = code
  224. self._failure_reason = reason
  225. self._completed_event_pending = False
  226. self._completed_event = EvtActionCompleted(action=self, state=self._state,
  227. failure_code=code,
  228. failure_reason=reason)
  229. self.dispatch_event(self._completed_event)
  230. def _set_aborting(self, log_abort_messages):
  231. if not self.is_running:
  232. raise ValueError("Action isn't currently running")
  233. if self._enable_abort_logging and log_abort_messages:
  234. logger.info('Aborting action=%s', self)
  235. self._state = ACTION_ABORTING
  236. #### Properties ####
  237. @property
  238. def is_running(self):
  239. '''bool: True if the action is currently in progress.'''
  240. return self._state == ACTION_RUNNING
  241. @property
  242. def is_completed(self):
  243. '''bool: True if the action has completed (either succeeded or failed).'''
  244. return self._state in (ACTION_SUCCEEDED, ACTION_FAILED)
  245. @property
  246. def is_aborting(self):
  247. '''bool: True if the action is aborting (will soon be either succeeded or failed).'''
  248. return self._state == ACTION_ABORTING
  249. @property
  250. def has_succeeded(self):
  251. '''bool: True if the action has succeeded.'''
  252. return self._state == ACTION_SUCCEEDED
  253. @property
  254. def has_failed(self):
  255. '''bool: True if the action has failed.'''
  256. return self._state == ACTION_FAILED
  257. @property
  258. def failure_reason(self):
  259. '''tuple of (failure_code, failure_reason): Both values will be None if no failure has occurred.'''
  260. return (self._failure_code, self._failure_reason)
  261. @property
  262. def result(self):
  263. """An attribute of :class:`ActionResults`: The result of running the action."""
  264. return self._result
  265. @property
  266. def state(self):
  267. '''string: The current internal state of the action as a string.
  268. Will match one of the constants:
  269. :const:`ACTION_IDLE`
  270. :const:`ACTION_RUNNING`
  271. :const:`ACTION_SUCCEEDED`
  272. :const:`ACTION_FAILED`
  273. :const:`ACTION_ABORTING`
  274. '''
  275. return self._state
  276. #### Private Event Handlers ####
  277. def _recv_msg_robot_completed_action(self, evt, *, msg):
  278. result = msg.result
  279. types = _clad_to_game_cozmo.ActionResult
  280. self._result = ActionResults.find_by_id(result)
  281. if self._result is None:
  282. logger.error("ActionResults has no entry for result id %s", result)
  283. if result == types.SUCCESS:
  284. # dispatch to the specific type to extract result info
  285. self._set_completed(msg)
  286. elif result == types.RUNNING:
  287. # XXX what does one do with this? it seems to occur after a cancel request!
  288. logger.warning('Received "running" action notification for action=%s', self)
  289. self._set_failed('running', 'Action was still running')
  290. elif result == types.NOT_STARTED:
  291. # not sure we'll see this?
  292. self._set_failed('not_started', 'Action was not started')
  293. elif result == types.TIMEOUT:
  294. self._set_failed('timeout', 'Action timed out')
  295. elif result == types.TRACKS_LOCKED:
  296. self._set_failed('tracks_locked', 'Action failed due to tracks locked')
  297. elif result == types.BAD_TAG:
  298. # guessing this is bad
  299. self._set_failed('bad_tag', 'Action failed due to bad tag')
  300. logger.error("Received FAILURE_BAD_TAG for action %s", self)
  301. elif result == types.CANCELLED_WHILE_RUNNING:
  302. self._set_failed('cancelled', 'Action was cancelled while running')
  303. elif result == types.INTERRUPTED:
  304. self._set_failed('interrupted', 'Action was interrupted')
  305. else:
  306. # All other results should fall under either the abort or retry
  307. # categories, determine the category by shifting the result
  308. result_category = result >> _clad_to_game_cozmo.ARCBitShift.NUM_BITS
  309. result_categories = _clad_to_game_cozmo.ActionResultCategory
  310. if result_category == result_categories.ABORT:
  311. self._set_failed('aborted', 'Action failed')
  312. elif result_category == result_categories.RETRY:
  313. self._set_failed('retry', 'Action failed but can be retried')
  314. else:
  315. # Shouldn't be able to get here
  316. self._set_failed('unknown', 'Action failed with unknown reason')
  317. logger.error('Received unknown action result status %s', msg)
  318. #### Public Event Handlers ####
  319. #### Commands ####
  320. def abort(self, log_abort_messages=False):
  321. '''Trigger the robot to abort the running action.
  322. Args:
  323. log_abort_messages (bool): True to log info on the action that
  324. is aborted.
  325. Raises:
  326. ValueError if the action is not currently being executed.
  327. '''
  328. self.robot._action_dispatcher._abort_action(self, log_abort_messages)
  329. async def wait_for_completed(self, timeout=None):
  330. '''Waits for the action to complete.
  331. Args:
  332. timeout (int or None): Maximum time in seconds to wait for the event.
  333. Pass None to wait indefinitely.
  334. Returns:
  335. The :class:`EvtActionCompleted` event instance
  336. Raises:
  337. :class:`asyncio.TimeoutError`
  338. '''
  339. if self.is_completed:
  340. # Already complete
  341. return self._completed_event
  342. return await self.wait_for(EvtActionCompleted, timeout=timeout)
  343. def on_completed(self, handler):
  344. '''Triggers a handler when the action completes.
  345. Args:
  346. handler (callable): An event handler which accepts arguments
  347. suited to the :class:`EvtActionCompleted` event.
  348. See :meth:`cozmo.event.add_event_handler` for more information.
  349. '''
  350. return self.add_event_handler(EvtActionCompleted, handler)
  351. class _ActionDispatcher(event.Dispatcher):
  352. _next_action_id = _clad_to_game_cozmo.ActionConstants.FIRST_SDK_TAG
  353. def __init__(self, robot, **kw):
  354. super().__init__(**kw)
  355. self.robot = robot
  356. self._in_progress = {}
  357. self._aborting = {}
  358. def _get_next_action_id(self):
  359. # Post increment _current_action_id (and loop within the SDK_TAG range)
  360. next_action_id = self.__class__._next_action_id
  361. if self.__class__._next_action_id == _clad_to_game_cozmo.ActionConstants.LAST_SDK_TAG:
  362. self.__class__._next_action_id = _clad_to_game_cozmo.ActionConstants.FIRST_SDK_TAG
  363. else:
  364. self.__class__._next_action_id += 1
  365. return next_action_id
  366. @property
  367. def aborting_actions(self):
  368. '''generator: yields each action that is currently aborting
  369. Returns:
  370. A generator yielding :class:`cozmo.action.Action` instances
  371. '''
  372. for _, action in self._aborting.items():
  373. yield action
  374. @property
  375. def has_in_progress_actions(self):
  376. '''bool: True if any SDK-triggered actions are still in progress.'''
  377. return len(self._in_progress) > 0
  378. @property
  379. def in_progress_actions(self):
  380. '''generator: yields each action that is currently in progress
  381. Returns:
  382. A generator yielding :class:`cozmo.action.Action` instances
  383. '''
  384. for _, action in self._in_progress.items():
  385. yield action
  386. async def wait_for_all_actions_completed(self):
  387. '''Waits until all actions are complete.
  388. In this case, all actions include not just in_progress actions but also
  389. include actions that we're aborting but haven't received a completed message
  390. for yet.
  391. '''
  392. while True:
  393. action = next(self.in_progress_actions, None)
  394. if action is None:
  395. action = next(self.aborting_actions, None)
  396. if action:
  397. await action.wait_for_completed()
  398. else:
  399. # all actions are now complete
  400. return
  401. def _send_single_action(self, action, in_parallel=False, num_retries=0):
  402. action_id = self._get_next_action_id()
  403. action.robot = self.robot
  404. action._action_id = action_id
  405. if self.has_in_progress_actions and not in_parallel:
  406. # Note - it doesn't matter if previous action was started as in_parallel,
  407. # starting any subsequent action with in_parallel==False will cancel
  408. # any previous actions, so we throw an exception here and require that
  409. # the client explicitly cancel or wait on earlier actions
  410. action = list(self._in_progress.values())[0]
  411. raise exceptions.RobotBusy('Robot is already performing %d action(s) %s' %
  412. (len(self._in_progress), action))
  413. if action.is_running:
  414. raise ValueError('Action is already running')
  415. if action.is_completed:
  416. raise ValueError('Action already ran')
  417. if in_parallel:
  418. position = _clad_to_game_cozmo.QueueActionPosition.IN_PARALLEL
  419. else:
  420. position = _clad_to_game_cozmo.QueueActionPosition.NOW
  421. qmsg = _clad_to_engine_iface.QueueSingleAction(
  422. idTag=action_id, numRetries=num_retries,
  423. position=position, action=_clad_to_engine_iface.RobotActionUnion())
  424. action_msg = action._encode()
  425. cls_name = action_msg.__class__.__name__
  426. # For some reason, the RobotActionUnion type uses properties with a lowercase
  427. # first character, instead of uppercase like all the other unions
  428. cls_name = cls_name[0].lower() + cls_name[1:]
  429. setattr(qmsg.action, cls_name, action_msg)
  430. self.robot.conn.send_msg(qmsg)
  431. self._in_progress[action_id] = action
  432. action._start()
  433. def _is_sdk_action_id(self, action_id):
  434. return ((action_id >= _clad_to_game_cozmo.ActionConstants.FIRST_SDK_TAG)
  435. and (action_id <= _clad_to_game_cozmo.ActionConstants.LAST_SDK_TAG))
  436. def _is_engine_action_id(self, action_id):
  437. return ((action_id >= _clad_to_game_cozmo.ActionConstants.FIRST_ENGINE_TAG)
  438. and (action_id <= _clad_to_game_cozmo.ActionConstants.LAST_ENGINE_TAG))
  439. def _is_game_action_id(self, action_id):
  440. return ((action_id >= _clad_to_game_cozmo.ActionConstants.FIRST_GAME_TAG)
  441. and (action_id <= _clad_to_game_cozmo.ActionConstants.LAST_GAME_TAG))
  442. def _action_id_type(self, action_id):
  443. if self._is_sdk_action_id(action_id):
  444. return "sdk"
  445. elif self._is_engine_action_id(action_id):
  446. return "engine"
  447. elif self._is_game_action_id(action_id):
  448. return "game"
  449. else:
  450. return "unknown"
  451. def _recv_msg_robot_completed_action(self, evt, *, msg):
  452. action_id = msg.idTag
  453. is_sdk_action = self._is_sdk_action_id(action_id)
  454. action = self._in_progress.get(action_id)
  455. was_aborted = False
  456. if action is None:
  457. action = self._aborting.get(action_id)
  458. was_aborted = action is not None
  459. if action is None:
  460. if is_sdk_action:
  461. logger.error('Received completed action message for unknown SDK action_id=%s', action_id)
  462. return
  463. else:
  464. if not is_sdk_action:
  465. action_id_type = self._action_id_type(action_id)
  466. logger.error('Received completed action message for sdk-known %s action_id=%s (was_aborted=%s)',
  467. action_id_type, action_id, was_aborted)
  468. action._completed_event_pending = True
  469. if was_aborted:
  470. if action._enable_abort_logging:
  471. logger.debug('Received completed action message for aborted action=%s', action)
  472. del self._aborting[action_id]
  473. else:
  474. logger.debug('Received completed action message for in-progress action=%s', action)
  475. del self._in_progress[action_id]
  476. # XXX This should generate a real event, not a msg
  477. # Should also dispatch to self so the parent can be notified.
  478. action.dispatch_event(evt)
  479. def _abort_action(self, action, log_abort_messages):
  480. # Mark this in-progress action as aborting - it should get a "Cancelled"
  481. # message back in the next engine tick, and can basically be considered
  482. # cancelled from now.
  483. action._set_aborting(log_abort_messages)
  484. if action._completed_event_pending:
  485. # The action was marked as still running but the ActionDispatcher
  486. # has already received a completion message (and removed it from
  487. # _in_progress) - the action is just waiting to receive a
  488. # robot_completed_action message that is still being dispatched
  489. # via asyncio.ensure_future
  490. logger.debug('Not sending abort for action=%s to engine as it just completed', action)
  491. else:
  492. # move from in-progress to aborting dicts
  493. self._aborting[action._action_id] = action
  494. del self._in_progress[action._action_id]
  495. msg = _clad_to_engine_iface.CancelActionByIdTag(idTag=action._action_id)
  496. self.robot.conn.send_msg(msg)
  497. def _abort_all_actions(self, log_abort_messages):
  498. # Mark any in-progress actions as aborting - they should get a "Cancelled"
  499. # message back in the next engine tick, and can basically be considered
  500. # cancelled from now.
  501. actions_to_abort = self._in_progress
  502. self._in_progress = {}
  503. for action_id, action in actions_to_abort.items():
  504. action._set_aborting(log_abort_messages)
  505. self._aborting[action_id] = action
  506. logger.info('Sending abort request for all actions')
  507. # RobotActionType.UNKNOWN is a wildcard that matches all actions when cancelling.
  508. msg = _clad_to_engine_iface.CancelAction(actionType=_clad_to_engine_cozmo.RobotActionType.UNKNOWN)
  509. self.robot.conn.send_msg(msg)