dashboard.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # Create a dash server that is also an actionlib client to turtle_actionlib
3 
4 from __future__ import print_function, division
5 
6 import os
7 import sys
8 import time
9 import json
10 import signal
11 import traceback
12 
13 import numpy as np
14 
15 from threading import Lock
16 
17 import rospy
18 import rospkg
19 import actionlib
20 
21 from actionlib_msgs.msg import GoalStatus
22 from turtlesim.msg import Pose
23 from turtle_actionlib.msg import ShapeAction, ShapeGoal
24 
25 # Plotly, Dash, and Flask
26 import plotly.graph_objs as go
27 
28 import dash
29 import dash_core_components as dcc
30 import dash_html_components as html
31 
32 from flask import jsonify
33 
34 
35 # Helper functions and constants (should ideally be in a utils module)
36 
37 GOAL_STATUS_TO_TXT = { getattr(GoalStatus, x): x for x in dir(GoalStatus) if x.isupper() }
38 
39 
40 # The app definition
41 
42 APP = dash.Dash(
43  __name__,
44  assets_folder=os.path.join(rospkg.RosPack().get_path('turtlesim_dash_tutorial'), 'dash_assets'),
45  external_stylesheets=[
46  {
47  'href': 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css',
48  'rel': 'stylesheet',
49  'integrity': 'sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T',
50  'crossorigin': 'anonymous',
51  },
52  ]
53 )
54 
55 
56 class Dashboard(object):
57  """
58  Create a Flask server to display the UI and a ROS node to send commands to
59  the turtlesim
60  """
61 
62  # Flask
63  APP_HOST = '0.0.0.0'
64  APP_PORT = 8080
65  APP_STATUS_URL = '/ros_api/status'
66  APP_STATUS_ENDPOINT = 'ros_status'
67 
68  # Actions, Topics, and Services
69  # Note that although the values are hard-coded for now, these can be set via
70  # service or ROS params if need be (a trivial update)
71  TURTLE_SHAPE_ACTION_NAME = 'turtle_shape'
72  TURTLE_POSE_TOPIC = '/turtle1/pose'
73 
74  # Constants that determine the behaviour of the dashboard
75  # Pose is published at ~62 Hz; so we'll see ~30 sec of history. Note that
76  # these parameters could be set through ROS parameters or services too!
77  POSE_UPDATE_INTERVAL = 5
78  POSE_MAX_TIMESTEPS = 2000
79  POSE_ATTRIBUTES = ['x', 'y', 'theta', 'linear_velocity', 'angular_velocity']
80 
81  # Constants for pertinent output fields
82  SERVER_STATUS_OUTPUT_FORMAT = "Shape Server Status: {status}"
83 
84  def __init__(self):
85  global APP
86 
87  # The Flask application
88  self._app = APP
89  self._flask_server = self._app.server
90 
91  # Create the stop signal handler
92  signal.signal(signal.SIGINT, self.stop)
93 
94  # Initialize the variables that we'll be using to save information
95  self._server_status = GoalStatus.LOST
96  self._pose_history = np.ones(
97  (1+len(Dashboard.POSE_ATTRIBUTES), Dashboard.POSE_MAX_TIMESTEPS)) * np.nan
98  self._history_length = 0
99  self._pose_history_lock = Lock()
100 
101  # Setup the subscribers, action clients, etc.
102  self._shape_client = actionlib.SimpleActionClient(Dashboard.TURTLE_SHAPE_ACTION_NAME, ShapeAction)
103  self._pose_sub = rospy.Subscriber(Dashboard.TURTLE_POSE_TOPIC, Pose, self._on_pose)
104 
105  # Initialize the application
106  self._define_app()
107 
108  @property
109  def pose_history(self):
110  return self._pose_history[:, :self._history_length]
111 
112  def start(self):
113  rospy.loginfo("Connecting to turtle_shape...")
114  self._shape_client.wait_for_server()
115  rospy.loginfo("...turtle_shape connected.")
116  self._app.run_server(host=Dashboard.APP_HOST,
117  port=Dashboard.APP_PORT,
118  debug=False)
119 
120  def stop(self, *args, **kwargs):
121  # Give some time for rospy to shutdown (cannot use rospy now!)
122  print("Shutting down Dash server")
123  time.sleep(2)
124  sys.exit(0)
125 
126  def _define_app(self):
127  """
128  Define the app layout and callbacks here
129  """
130  # Define each component of the page
131 
132  # First the graph element that will plot the pose and velocity of the
133  # robot
134  pose_graph_layout = html.Div(dcc.Graph(id='pose', style={ 'width': '100%' }), className='row')
135 
136  # Then the section that will update the parameters for the shape that
137  # the turtle will trace in the turtle sim
138  shape_params_layout = html.Div(
139  [
140  dcc.Input(id="shape-edges", type='number', placeholder='Num Edges', className='col mx-2'),
141  dcc.Input(id="shape-radius", type='number', placeholder='Radius', className='col mx-2'),
142  html.Button("Trace Shape", id='trace-button', n_clicks=0, className='btn btn-large btn-primary col-3'),
143  ],
144  className='row'
145  )
146 
147  # Then the section that will display the status of the shape server
148  server_status_layout = html.Div(
149  dcc.Markdown(id='server-status', className='col'),
150  className='row my-2'
151  )
152 
153  # String them all together in a single page
154  self._app.layout = html.Div(
155  [
156  # Hidden button for JS polling
157  html.Button(id='refresh-status', n_clicks=0, style={ 'display': 'none' }),
158 
159  # The params for tracing the shape
160  html.Div(html.H3('Shape Tracing:', className='col'), className='row mt-4'),
161  shape_params_layout,
162  server_status_layout,
163 
164  # The section showing the action status
165  html.Div(html.H3('Pose History:', className='col'), className='row my-2'),
166  pose_graph_layout,
167 
168  # The interval component to update the plots
169  dcc.Interval(id='interval-component',
170  n_intervals=0,
171  interval=(Dashboard.POSE_UPDATE_INTERVAL * 1000)),
172  ],
173  className="container"
174  )
175 
176  # Define callbacks to update the elements on the page
177  self._app.callback(
178  dash.dependencies.Output('pose', 'figure'),
179  [dash.dependencies.Input('interval-component', 'n_intervals')]
181 
182  # Define a callback to send the goal to the server when the 'Trace'
183  # button is clicked. Wait until the client is done executing
184  self._app.callback(
185  dash.dependencies.Output('trace-button', 'autoFocus'),
186  [dash.dependencies.Input('trace-button', 'n_clicks')],
187  [dash.dependencies.State('shape-edges', 'value'),
188  dash.dependencies.State('shape-radius', 'value')]
190 
191  # Define a callback to show the status of the server
192  self._app.callback(
193  dash.dependencies.Output('server-status', 'children'),
194  [dash.dependencies.Input('refresh-status', 'n_clicks')]
196 
197  # Add the flask API endpoints
198  self._flask_server.add_url_rule(
199  Dashboard.APP_STATUS_URL,
200  Dashboard.APP_STATUS_ENDPOINT,
202  )
203 
205  """
206  Define a callback to populate the server status display when the status
207  refresh button (hidden) is pressed
208  """
209  def server_status_callback(n_clicks):
210  status = GOAL_STATUS_TO_TXT.get(self._server_status)
211  return Dashboard.SERVER_STATUS_OUTPUT_FORMAT.format(**locals())
212 
213  return server_status_callback
214 
216  """
217  Define a callback that will be invoked every time the 'Trace' button is
218  clicked.
219  """
220  def trace_shape_callback(n_clicks, num_edges, radius):
221  # Ignore the 'click' event when the component is created
222  if n_clicks is None or n_clicks == 0:
223  return False
224 
225  # Coerce the input data into formats that we can use
226  try:
227  num_edges = int(num_edges)
228  radius = float(radius)
229  except Exception as e:
230  rospy.logerr("Error parsing params - {}\n{}".format(e, traceback.format_exc()))
231  return False
232 
233  # Create the goal and send it to the action server
234  goal = ShapeGoal(edges=num_edges, radius=radius)
235  self._shape_client.send_goal(goal)
236  self._server_status = GoalStatus.ACTIVE
237 
238  # Wait for a result
239  self._shape_client.wait_for_result()
240 
241  # Finally, update the status, log the result, and return true
242  self._server_status = self._shape_client.get_state()
243  result = self._shape_client.get_result()
244  rospy.loginfo("ShapeServer: Interior Angle - {result.interior_angle}, Apothem - {result.apothem}".format(**locals()))
245 
246  return True
247 
248  return trace_shape_callback
249 
251  """
252  Define a callback that will be invoked on every update of the interval
253  component. Keep in mind that we return a callback here; not a result
254  """
255  def pose_history_callback(n_intervals):
256  # Get a view into the latest pose history
257  pose_history = self.pose_history
258 
259  # Create the output graph
260  data = [
261  go.Scatter(
262  name=attr,
263  x=pose_history[0, :],
264  y=pose_history[idx+1, :],
265  mode='lines+markers'
266  )
267  for idx, attr in enumerate(Dashboard.POSE_ATTRIBUTES)
268  ]
269  layout = go.Layout(
270  showlegend=True,
271  height=500,
272  yaxis=dict(
273  fixedrange=True
274  ),
275  margin=dict(
276  autoexpand=True
277  )
278  )
279 
280  return { 'data': data, 'layout': layout }
281 
282  return pose_history_callback
283 
284  def _on_pose(self, msg):
285  """
286  The callback for the position of the turtle on
287  :const:`TURTLE_POSE_TOPIC`
288  """
289  if self._history_length == Dashboard.POSE_MAX_TIMESTEPS:
290  self._pose_history[:, :-1] = self._pose_history[:, 1:]
291  else:
292  self._history_length += 1
293 
294  self._pose_history[:, self._history_length-1] = [
295  rospy.Time.now().to_time() % 1000,
296  msg.x,
297  msg.y,
298  msg.theta,
299  msg.linear_velocity,
300  msg.angular_velocity,
301  ]
302 
304  return jsonify({
305  'server_status': self._server_status,
306  })


turtlesim_dash_tutorial
Author(s):
autogenerated on Mon Feb 28 2022 23:55:19