17 from threading
import Event
26 ScriptableBase is a superclass that has common functionality for Scriptable... callbacks.
28 It allows to set a default reply that can be overridden if need be, so custom replies can be defined
31 def __init__(self, name, goal_formatter=format, reply_formatter=format, default_reply=None, default_reply_delay=0):
33 Set up a ScriptableBase. Must be subclassed.
35 :param name: the action namespace the server should operate on (e.g. `move_base`)
36 :param goal_formatter: a function accepting a goal and returning a nice-looking summary str of it
37 :param reply_formatter: a function accepting a result and returning a nice-looking summary str of it
38 :param default_reply: optional. If set, this will be returned after default_result_delay,
39 otherwise a .reply* call is needed to send a reply. Overriding the default reply and
40 doing something custom is possible with the `custom_reply`-context manager
41 :param default_reply_delay: If default_result is defined, the Scriptable... waits for this delay before returning.
42 This delay is also used when no more specific reply_delay is specified in a reply_*-call
79 If the process is blocked by waiting for some Events to be set, stop sets those Events.
84 def reply(self, result, timeout=None, reply_delay=None, marker=None):
86 Reply to the next goal with the given result, after `reply_delay` amount of seconds.
87 An AssertionError is raised when a goal is not received within the given timeout.
89 :param result: an ...ActionResult of the type associated with the Action-type of this server
90 :param timeout: how long to wait for the goal? Defaults to None to wait indefinitely
91 :param reply_delay: how to to reply_delay/calculate on this goal before sending the reply
92 :param marker: A str that is printed in the output for easy reference between different replies
95 if reply_delay
is None:
98 print(
'\n######## {}.reply{} ###########'.format(self.
_name,
'({})'.format(marker)
if marker
else ''))
100 assert self.
_waiting_for is None,
"reply{} cannot follow an 'await_goal', use reply_directly".format(
'({})'.format(marker)
if marker
else '')
103 print(
"{}.reply{}: Waiting {}for goal...".format(self.
_name,
'({})'.format(marker)
if marker
else '',
104 str(timeout)+
's ' if timeout
is not None else ''))
106 assert self.
_request.wait(timeout), \
107 "{}.reply{} did not get a goal in time".format(self.
_name,
'({})'.format(marker)
if marker
else '')
110 print(
"{}.reply{}: Got goal: {}"
116 countdown_sleep(reply_delay, text=
"{}.reply{}: Think for {}s. ".format(self.
_name,
'({})'.format(marker)
if marker
else '', reply_delay) +
"Remaining {}s...")
122 print(
"{}.reply{}: Finished replying"
123 .format(self.
_name,
'({})'.format(marker)
if marker
else ''))
125 def reply_conditionally(self, condition, true_result, false_result, timeout=None, reply_delay=None, marker=None):
127 Reply one of two possibilities, based on a condition. This is a callable that, given a Goal, returns a bool
128 If True, then reply with the true_reply and vice versa.
129 An AssertionError is raised when a goal is not received within the given timeout.
131 :param condition: callable(...Goal) -> bool
132 :param true_result: a ...Result
133 :param false_result: a ...Result
134 :param timeout: seconds to wait for the goal. Defaults to None to wait indefinitely
135 :param reply_delay: Delay the reply by this amount of seconds
136 :param marker: A str that is printed in the output for easy reference between different replies
139 if reply_delay
is None:
142 print(
'\n######## {}.reply_conditionally{} ###########'
143 .format(self.
_name,
'({})'.format(marker)
if marker
else ''))
145 assert self.
_waiting_for is None,
"reply_conditionally{} cannot follow an 'await_goal', use reply_directly".format(
'({})'.format(marker)
if marker
else '')
148 print(
"{}.reply_conditionally{}: Waiting {}for goal..."
149 .format(self.
_name,
'({})'.format(marker)
if marker
else '',
150 str(timeout)+
's ' if timeout
is not None else ''))
151 assert self.
_request.wait(timeout),
"{}.reply_conditionally{} did not get a goal in time"\
152 .format(self.
_name,
'({})'.format(marker)
if marker
else '')
155 print(
"{}.reply_conditionally{}: Got goal: {}"
158 print(
"{}: Think for {}s...".format(self.
_name, reply_delay))
164 countdown_sleep(reply_delay, text=
"{}.reply_conditionally{}: Think for {}s. "
165 .format(self.
_name,
'({})'.format(marker)
if marker
else '', reply_delay) +
"Remaining {}s...")
172 print(
"{}.reply_conditionally{}: Finished replying"
173 .format(self.
_name,
'({})'.format(marker)
if marker
else ''))
179 Await a goal to be sent to this Scriptable... and return that goal for close inspection.
180 Based on that, send a reply via `direct_reply`
181 An AssertionError is raised when a goal is not received within the given timeout.
183 :param timeout: how long to wait for the goal? Defaults to None to wait indefinitely
184 :param marker: A str that is printed in the output for easy reference between different replies
185 :return: the received goal
187 print(
'\n######## {}.await_goal{} ###########'
188 .format(self.
_name,
'({})'.format(marker)
if marker
else ''))
191 print(
"{}.await_goal{}: Waiting {}for goal..."
192 .format(self.
_name,
'({})'.format(marker)
if marker
else '',
193 str(timeout)+
's ' if timeout
is not None else ''))
194 assert self.
_request.wait(timeout),
"{}.await_goal{} did not get a goal in time".format(self.
_name,
'({})'.format(marker)
if marker
else '')
196 print(
"{}.await_goal{}: Got goal: {}"
205 Reply to the current goal with the given result, after `reply_delay` amount of seconds.
207 :param result: a ...Result of the type associated with the type of this server
208 :param reply_delay: how long to 'reply_delay/calculate' on this goal before sending the reply
209 :param marker: A str that is printed in the output for easy reference between different replies
211 assert self.
_waiting_for ==
'direct_reply',
"reply cannot follow an 'await_goal', use reply_directly"
213 if reply_delay
is None:
220 .format(self.
_name,
'({})'.format(marker)
if marker
else '', reply_delay) +
"Remaining {}s...")
228 print(
"{}.direct_reply{}: Finished replying".format(self.
_name,
'({})'.format(marker)
if marker
else ''))
233 The Result that is currently set to be returned by default
237 @default_reply.setter
240 Set the current default reply.
241 If this is None, there will not be a default reply, then a reply must be defined via a reply*-call
243 :param result: a ...Result of the type associated with the type of this server
250 Wait this amount of time before returning the default reply
254 @default_reply_delay.setter
256 """Set the delay after which the `default_result` is sent
258 :param delay number of seconds to wait before sending the `default_result`"""
261 @contextlib.contextmanager
264 Use this context manager to temporarily define your own replies and go back to defaulting outside the context
268 >>> server = ScriptableBase()
269 >>> server.default_result = True
270 >>> # Do other stuff that does not require special attention of server
271 >>> with server.custom_reply():
272 >>> server.reply(False)
273 >>> # Continue to do other stuff that does not require special attention of server
283 Clear the log of goals that we've received
290 The goal we've received last
297 all goals received (since the last `clear_goals()` call
303 Add a callback that is called just before a result is being sent back
305 :param callback: callable that will receive the goal and result. The return value of the callable is discarded
307 assert isinstance(callback, collections.Callable)
312 Trigger all callback in the order they were added, with the current goal and the reply to it
314 :param goal: current goal
315 :param reply: reply to that goal
322 Find out if this server has any goal in it's history that is also in `match_against`
324 :param match_against: We're looking for any of the items in `match_against`
325 :param key: optionally transform the received goals with this callable into the same type as `match_against`'s elements
326 :return: The matching elements
328 assert isinstance(key, collections.Callable),
"key must be a callable"
331 return set(match_against).intersection(set(processed_previous_goals))
333 @contextlib.contextmanager
336 Remember goals only instance of this context.
337 Goals before this context are forgotten;
338 Goals received during/in the context only are remembered;
339 after the context everything is forgotten.