diff --git a/Dockerfile b/Dockerfile index 1872c6e..0406c55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,6 @@ RUN . /opt/ros/foxy/setup.sh \ && colcon build --packages-select starling_ui_dashly starling_allocator_msgs --cmake-force-configure \ && rm -r build -EXPOSE 3000 +ENV PORT 3000 cmd ["ros2", "launch", "starling_ui_dashly", "dashboard_gunicorn.launch.xml"] \ No newline at end of file diff --git a/README.md b/README.md index 5f75249..f75b10c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ ros2 launch starling_ui_dashly dashboard_gunicorn.launxch.xml which will run a user facing server on https://0.0.0.0:3000 +> The port number can be specified with environment varibale `PORT` + ### Docker The project can also be run using a dockerfile, either build the local file after cloning as follows @@ -48,6 +50,10 @@ Or use the built version on docker hub docker run --it -rm --network=host mickeyli789/starling_ui_dashly:latest ``` +> Specify port number using `-e PORT=3002` for example. + +To make things easier, a MAKEFILE has been provided. Therefore to build and run the project, run `make run` in the root of this project. + ## Usage ### Mission Control Screen (`/`) diff --git a/starling_ui_dashly/launch/dashboard_gunicorn.launch.xml b/starling_ui_dashly/launch/dashboard_gunicorn.launch.xml index 8768d4e..a1739a7 100644 --- a/starling_ui_dashly/launch/dashboard_gunicorn.launch.xml +++ b/starling_ui_dashly/launch/dashboard_gunicorn.launch.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/starling_ui_dashly/starling_ui_dashly/control_panel_manager_component.py b/starling_ui_dashly/starling_ui_dashly/control_panel_manager_component.py index 132e529..60c367e 100644 --- a/starling_ui_dashly/starling_ui_dashly/control_panel_manager_component.py +++ b/starling_ui_dashly/starling_ui_dashly/control_panel_manager_component.py @@ -1,7 +1,8 @@ import pandas as pd +import json import dash -from dash.dependencies import Input, Output, State +from dash.dependencies import Input, Output, State, MATCH, ALL from dash import dcc, html import dash_bootstrap_components as dbc # import dash_core_components as dcc @@ -123,21 +124,51 @@ def __control_panel_system_status_update(self, n_clicks, n_intervals): # Call self.dashboard_node.get_system_status() vehic_namespace = self.dashboard_node.get_current_vehicle_namespaces() return html.Div([ - html.P(f"System status update at {get_time()}"), + html.P(f"System status update at {get_time()}, {len(vehic_namespace)} vehicles detected"), dbc.Row([ dbc.Col([ - html.H4(f"Abort {vehicle_name}"), - dbc.Button( - html.Img(src='/static/stop-button.png', style={"max-width": "75%"}), - id=f"mc_btn_mission_abort_{vehicle_name}", - size="sm", - outline=True, - color="warning"), + html.Div([ + dbc.Button( + f"Abort {vehicle_name}", + id={'type': f"mc_btn_mission_abort_drone", 'index': vehicle_name}, + size="lg", + block=True, + color="warning"), + dbc.Button( + f"ESTOP {vehicle_name}", + id={'type': f"mc_btn_emergency_stop_drone", 'index': vehicle_name}, + size="lg", + block=True, + color="danger"), + ], + className="d-grid gap-5") ]) for vehicle_name in vehic_namespace - ]) + ]), + html.Div("", id="mc_system_status_individual_vehicle_status_msg") ]) + @app_callback( + Output("mc_system_status_individual_vehicle_status_msg", "children"), + [Input({"type": "mc_btn_mission_abort_drone", "index": ALL}, 'n_clicks'), + Input({"type": "mc_btn_emergency_stop_drone", "index": ALL}, 'n_clicks')] + ) + def __control_panel_individual_drone_abort_press(self, abort_n_clicks, estop_n_clicks): + ctx = dash.callback_context + if ctx.triggered: + trigger_id = ctx.triggered[0]['prop_id'].split('.')[0] + trigger_dict = json.loads(trigger_id) + vehicle_name = trigger_dict["index"] + + if trigger_dict["type"] == "mc_btn_mission_abort_drone": + self.dashboard_node.call_mission_abort_drone(vehicle_name) + return f"Abort {vehicle_name} pressed at {get_time()}" + else: + self.dashboard_node.send_emergency_stop_drone(vehicle_name) + return f"ESTOP {vehicle_name} pressed at {get_time()}" + + return "Waiting for button press" + def __generate_control_panel_emergency_stop(self): return html.Div([ html.H2("EMERGENCY STOP (/emergency_stop)", id="mc_title_estop"), diff --git a/starling_ui_dashly/starling_ui_dashly/main.py b/starling_ui_dashly/starling_ui_dashly/main.py index 2280efb..7646a9c 100644 --- a/starling_ui_dashly/starling_ui_dashly/main.py +++ b/starling_ui_dashly/starling_ui_dashly/main.py @@ -24,7 +24,7 @@ try: from .node import Dashboard_Node from .handler import Dashboard_Handler -except Exception: +except ModuleNotFoundError: from node import Dashboard_Node from handler import Dashboard_Handler diff --git a/starling_ui_dashly/starling_ui_dashly/node.py b/starling_ui_dashly/starling_ui_dashly/node.py index 897d8eb..a37e84b 100644 --- a/starling_ui_dashly/starling_ui_dashly/node.py +++ b/starling_ui_dashly/starling_ui_dashly/node.py @@ -28,6 +28,8 @@ def __init__(self): self.timer_period = 0.01 # seconds self.emergency_stop_timer = None + self.current_vehicle_namespaces = [] + self.valid_methods = [ 'nearest', 'random', @@ -62,7 +64,7 @@ def send_emergency_stop_drone(self, drone_namespace): msg = Empty() emergency_stop_publisher_ = self.create_publisher(Empty, f'/{drone_namespace}/emergency_stop', 10) emergency_stop_publisher_.publish(msg) - self.get_logger().info('emergency_stop published') + self.get_logger().info(f'emergency_stop published to {drone_namespace}') emergency_stop_publisher_.destroy() def call_mission_abort_drone(self, drone_namespace): @@ -75,16 +77,17 @@ def call_mission_abort_drone(self, drone_namespace): def get_current_vehicle_namespaces(self): topic_list = self.get_topic_names_and_types() namespaces = set() - self.get_logger().info('Found the following topics:') + # self.get_logger().info('Found the following topics:') for topic_name, _ in topic_list: - self.get_logger().info(topic_name) - if 'mavros' in topic_name: + # self.get_logger().info(topic_name) + if 'mavros/state' in topic_name: name = topic_name.split('/')[1] if name == 'mavros': name = '' namespaces.add(name) self.get_logger().info(f'Found {len(namespaces)} namespaces: {",".join(namespaces)}') - return list(namespaces) + self.current_vehicle_namespaces = list(namespaces) + return self.current_vehicle_namespaces def is_valid_trajectory_dict(self, trajectory_dict): self.get_logger().info("testing validity of trajectory dictionary")