robot_routine.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 import rospy
4 
5 from strands_executive_msgs import task_utils
6 from strands_executive_msgs.msg import Task, ExecutionStatus
7 from strands_executive_msgs.srv import AddTasks, SetExecutionStatus, DemandTask
8 from std_srvs.srv import Empty
9 from mongodb_store.message_store import MessageStoreProxy
10 
11 from scitos_msgs.msg import BatteryState
12 
13 from dateutil.tz import *
14 from datetime import *
15 
16 from task_executor import task_routine
17 from task_executor.task_routine import delta_between
18 from copy import deepcopy
19 
20 from dynamic_reconfigure.server import Server
21 from routine_behaviours.cfg import ChargingThresholdsConfig
22 
23 
24 class RobotRoutine(object):
25  """ This class manages the over-arching routine behaviour of the robot. It monitors battery charge task level and can force charging when necessary and also generate tasks when the robot is idle. """
26 
27  def __init__(self, daily_start, daily_end, idle_duration, charging_point = 'ChargingPoint'):
28  """
29  Args:
30  daily_start (datetime.time): The time of day the routine should start.
31  daily_end (datetime.time): The time of day the routine should end.
32  idle_duration (rospy.Duration): How long to be idle before triggering on_idle.
33  charging_point: Where to head to when charge is low
34  """
35 
36  self.daily_start = daily_start
37  self.daily_end = daily_end
38  self.charging_point = charging_point
39  self._create_services()
40 
41  rospy.loginfo('Fetching parameters from dynamic_reconfigure')
42  self.recfg_sever = Server(ChargingThresholdsConfig, self.dynamic_reconfigure_cb)
43 
44 
45  self.battery_count = 0
46  # home many ~10Hz updates to wait for between forced charge counts
47  self.battery_count_thres = 10 * 60 * 5
48  self.battery_state = None
49  # how long to charge for when force_charge_threshold is triggered
50  self.force_charge_duration = rospy.Duration(60 * 60 * 2)
51  self.sent_night_tasks = False
52  self.night_tasks = []
53 
54  rospy.Subscriber('/battery_state', BatteryState, self._check_battery)
55  # lazy/silly sleep to check battery received if present
56  rospy.sleep(0.5)
57 
58 
59 
60  # create routine structure
61  self.routine = task_routine.DailyRoutine(daily_start, daily_end)
62  # create the object which will talk to the scheduler
63  self.runner = task_routine.DailyRoutineRunner(self.daily_start, self.daily_end, self.add_tasks, day_start_cb=self.on_day_start, day_end_cb=self.on_day_end, tasks_allowed_fn=self.task_allowed_now)
64 
65 
66  # calculate how long to sleep for overnight
67  start_time_secs = task_routine.time_to_secs(daily_start)
68  end_time_secs = task_routine.time_to_secs(daily_end)
69  full_day_secs = task_routine.time_to_secs(time(23,59))
70 
71  # this is the whole night
72  night_secs = (full_day_secs - end_time_secs) + start_time_secs
73  self.night_duration = rospy.Duration(night_secs)
74 
75 
76 
77  # how big a gap in the scheduler should trigger a return to charger
78  self.charge_window = rospy.Duration(60 * 10)
79 
80  # and how long to charge for once there, it won't automatically leave if there's nothing to do
81  self.idle_charge_duration = rospy.Duration(60)
82 
83  self.idle_count = 0
84 
85  # how many 5 second idle counts to wait for before deciding we're idle
86  # a count of 12 is one minute
87 
88 
89 
90  self.idle_thres = int(idle_duration.to_sec() / 5)
91 
92  self.sent_to_charge = False
93 
94  # Set the task executor running in case it's not
95  self.set_execution_status(True)
96 
97  rospy.Subscriber('current_schedule', ExecutionStatus, self._check_idle)
98 
99 
100  def task_allowed_now(self, task):
101  """
102  Return true if a task can be send to the task_executor now.
103 
104 
105  This checks if battery level is fine and that the robot is not at the charging point. Subclasses can override this to provide additional checks.
106 
107  """
108  return self.battery_ok() or task.start_node_id == self.charging_point
109 
110  def dynamic_reconfigure_cb(self, config, level):
111  rospy.loginfo("Config set to {force_charge_threshold}, {force_charge_addition}".format(**config))
112 
113  self.threshold = config['force_charge_threshold']
114  self.addition = config['force_charge_addition']
115  return config
116 
117  def add_night_task(self, task):
118  """
119  Add a task to be executed after the routine ends. These tasks cannot involve movement and therefore must either have an empty start_node_id or be performed at the charging station.
120  """
121  if task.start_node_id == charging_point:
122  task.start_node_id = ''
123 
124  if task.start_node_id != '':
125  rospy.logwarn('Rejecting task to do %s at %s as only location-free tasks are allowed at night')
126  return
127 
128  self.night_tasks.append(task)
129 
130  def is_before_day_start(self,time):
131  """
132  Check if the given time is before the start of the routine.
133  """
134  if task_routine.time_less_than(self.daily_start,self.daily_end):
135  return task_routine.time_less_than(time, self.daily_start)
136  else:
137  return task_routine.time_less_than(time, self.daily_start) and task_routine.time_less_than(self.daily_end, time)
138 
139  def is_after_day_end(self,time):
140  """
141  Check if the given time is after the end of the routine.
142  """
143  if task_routine.time_less_than(self.daily_start,self.daily_end):
144  return task_routine.time_less_than(self.daily_end, time)
145  else:
146  return task_routine.time_less_than(self.daily_end, time) and task_routine.time_less_than(time, self.daily_start)
147 
148  def is_during_day(self,time):
149  """
150  Check if the given time is during the routine.
151  """
152  return not (self.is_before_day_start(time) or self.is_after_day_end(time))
153 
154 
155  def _check_idle(self, schedule):
156 
157  # only check within working hours
158  rostime_now = rospy.get_rostime()
159  now = datetime.fromtimestamp(rostime_now.to_sec(), tzlocal()).time()
160 
161  # if the task to get the robot on to the charge station fails is some way it could
162  # get strandard. The battery recovery will kick in later, but we need to keep trying
163  # in order to get the night tasks launched, which are only sent when charging
164 
165 
166  on_charger = self.battery_state is not None and (self.battery_state.charging or self.battery_state.powerSupplyPresent)
167 
168  # if it's night time, we're not doing anything and we're not on the charger
169  if not self.is_during_day(now) and not schedule.currently_executing and not on_charger:
170  self.demand_charge(rospy.Duration(10 * 60.0))
171 
172  elif self.is_during_day(now) and self.battery_ok() :
173 
174  if schedule.currently_executing:
175  self.idle_count = 0
176  return
177  if len(schedule.execution_queue) == 0:
178  self.idle_count += 1
179  else:
180  # delay until next task
181  delay_until_next = schedule.execution_queue[0].execution_time - rostime_now
182  # rospy.loginfo('delay until next: %s' % delay_until_next.to_sec())
183  if delay_until_next > self.charge_window:
184  self.idle_count += 1
185  else:
186  self.idle_count = 0
187 
188  else:
189  self.idle_count = 0
190 
191  # rospy.loginfo('idle count: %s' % self.idle_count)
192  # rospy.loginfo('idle threshold: %s' % self.idle_thres)
193 
194  if self.idle_count > self.idle_thres:
195  self.on_idle()
196 
197 
198  def battery_ok(self):
199  """ Reports false if battery is below force_charge_threshold or if it is above it but within force_charge_addition of the threshold and charging """
200 
201  if self.battery_state is not None:
202 
203  # if batter is below threshold, it's never ok
204  if self.battery_state.lifePercent < self.threshold:
205  return False
206  # else if we're charging we should allow some amount of charging to happen
207  # before everything is ok again
208  elif self.battery_state.charging or self.battery_state.powerSupplyPresent:
209  threshold = min(self.threshold + self.addition, 99)
210  return self.battery_state.lifePercent > threshold
211  else:
212  return True
213 
214  else:
215  rospy.logwarn('No battery received when checking')
216  return True
217 
218  def _check_battery(self, battery_state):
219  self.battery_state = battery_state
220 
221 
222  if not self.battery_ok():
223 
224  # if not ok and not triggered yet
225  if self.battery_count == 0:
226  rospy.logwarn('Battery below force charge threshold: %s ' % (self.battery_state.lifePercent))
228 
229  # update count
230  self.battery_count += 1
231 
232  if self.battery_count >= self.battery_count_thres and not (self.battery_state.charging or self.battery_state.powerSupplyPresent):
233  rospy.logwarn('Still not charging, trying again')
234  self.battery_count = 0
235  else:
236  self.battery_count = 0
237 
238  # check for charging so we can send off night tasks
239  rostime_now = rospy.get_rostime()
240  now = datetime.fromtimestamp(rostime_now.to_sec(), tzlocal()).time()
241 
242  if len(self.night_tasks) > 0 and not self.sent_night_tasks and self.battery_state is not None and self.battery_state.charging and not self.is_during_day(now):
243  rospy.loginfo('Sending night tasks')
244  self._send_night_tasks()
245 
246  def _send_night_tasks(self):
247  instantiated_night_tasks = []
248 
249  now = rospy.get_rostime()
250 
251  for task in self.night_tasks:
252  night_task = deepcopy(task)
253  night_task.start_after = now
254  # some arbitraty time -- 6 hours -- in the future
255  night_task.end_before = now + rospy.Duration(60 * 60 * 12)
256  instantiated_night_tasks.append(night_task)
257 
258 
259  self.add_tasks(instantiated_night_tasks)
260  self.sent_night_tasks = True
261 
262  def _create_services(self):
263  add_tasks_srv_name = '/task_executor/add_tasks'
264  set_exe_stat_srv_name = '/task_executor/set_execution_status'
265  demand_task_srv_name = '/task_executor/demand_task'
266  clear_schedule_srv_name = '/task_executor/clear_schedule'
267  rospy.loginfo("Waiting for task_executor service...")
268  rospy.wait_for_service(add_tasks_srv_name)
269  rospy.wait_for_service(set_exe_stat_srv_name)
270  rospy.loginfo("Done")
271  self.add_tasks = rospy.ServiceProxy(add_tasks_srv_name, AddTasks)
272  self.set_execution_status = rospy.ServiceProxy(set_exe_stat_srv_name, SetExecutionStatus)
273  self.demand_task = rospy.ServiceProxy(demand_task_srv_name, DemandTask)
274  self.clear_schedule = rospy.ServiceProxy(clear_schedule_srv_name, Empty)
275  self.maps_msg_store = MessageStoreProxy(collection='topological_maps')
276 
277 
278  # deprecated
279  # def load_nodes(self):
280  # msg_store = MessageStoreProxy(collection='topological_maps')
281  # query_meta = {}
282  # query_meta["pointset"] = rospy.get_param('topological_map_name')
283  # nodes = self.maps_msg_store.query(TopologicalNode._type, {}, query_meta)
284  # return [n for [n, meta] in nodes]
285 
286  def demand_charge(self, charge_duration):
287  """
288  Create an on-demand task to charge the robot for the given duration.
289  """
290  charging_point = self.charging_point
291  charge_task = Task(action='wait_action', start_node_id=charging_point, end_node_id=charging_point, max_duration=charge_duration)
292  task_utils.add_time_argument(charge_task, rospy.Time())
293  task_utils.add_duration_argument(charge_task, charge_duration)
294  self.demand_task(charge_task)
295 
296  def clear_then_charge(self, charge_duration):
297  """ Clears the schedule of the robot before demanding a charge task. """
298  try:
299  self.clear_schedule()
300  except rospy.ServiceException, e:
301  rospy.loginfo('Empty service complaint occurs here. Should be safe: %s'% e)
302 
303  # safety sleep, but could be issues here
304  rospy.sleep(10)
305  self.demand_charge(charge_duration)
306 
307  def on_day_end(self):
308  """
309  Triggered when the routine ends for the day.
310 
311  This clears tasks and triggers an on-demand charge.
312  """
313  # end of day, charge for 10 mins; will charge all night as no moves allowed.
314  self.clear_then_charge( rospy.Duration(10 * 60.0) )
315 
316  rospy.loginfo('ok, I\'m calling it a day until %s' % datetime.fromtimestamp((rospy.get_rostime() + self.night_duration).to_sec()))
317 
318 
319  def on_day_start(self):
320  """
321  Triggered when the routine starts for the day.
322  """
323  rospy.loginfo('Good morning')
324  self.sent_night_tasks = False
325 
326  def start_routine(self):
327  """
328  Starts the routine running. Must be called to generate behaviour.
329  """
330  self.runner.add_tasks(self.routine.get_routine_tasks())
331 
332  def on_idle(self):
333  """
334  Called when the routine is idle. Default is to trigger travel to the charging. As idleness is determined by the current schedule, if this call doesn't utlimately cause a task schedule to be generated this will be called repeatedly.
335  """
336  rospy.loginfo('Idle for too long, going to charging point')
337  # go for a quick charge
339 
340  def create_task_routine(self, tasks, daily_start=None, daily_end=None, repeat_delta=None):
341  """
342  Create routine behaviour from given tasks using provided parameter.
343 
344  This largely passes arguments on to similar arguments in task_routine.repeat_every_delta, but constrains repeats to the bounds of the this routine.
345 
346  If daily start or end not supplied use routine start or end
347  If delta not supplied, just do once during the start to end window
348  """
349 
350  if daily_start is None:
351  daily_start = self.daily_start
352 
353  if daily_end is None:
354  daily_end = self.daily_end
355 
356  if repeat_delta is None:
357  repeat_delta = delta_between(daily_start, daily_end)
358 
359 
360  self.routine.repeat_every_delta(tasks, delta=repeat_delta, times=1, start_time=daily_start, duration=delta_between(daily_start, daily_end))
361 
362 
363 
364 
def clear_then_charge(self, charge_duration)
def dynamic_reconfigure_cb(self, config, level)


routine_behaviours
Author(s): Nick Hawes
autogenerated on Tue Mar 17 2015 21:43:31