From c74490c605e9e668aadbe6d93852df02c16ebc38 Mon Sep 17 00:00:00 2001 From: Amal Nanavati Date: Fri, 5 May 2023 12:59:34 -0700 Subject: [PATCH] Implemented and Tested MoveAbovePlate - Implemented and tested the MoveAbovePlate dummy ROS action - Integrated it with the web app and thoroughly tested it - Made the Footer take in paused/setPaused and callback functions from the page it is on - Updated README --- feeding_web_app_ros2_msgs/package.xml | 2 +- .../MoveAbovePlate.py | 20 ++ .../feeding_web_app_ros2_test/MoveToDummy.py | 223 ++++++++++++++++++ feeding_web_app_ros2_test/package.xml | 2 +- feeding_web_app_ros2_test/setup.py | 5 +- feedingwebapp/README.md | 24 +- feedingwebapp/TechDocumentation.md | 8 + feedingwebapp/src/Pages/Constants.js | 26 ++ feedingwebapp/src/Pages/Footer/Footer.jsx | 191 ++++++--------- .../Pages/Home/MealStates/BiteAcquisition.jsx | 29 ++- .../Home/MealStates/MovingAbovePlate.jsx | 212 ++++++++++++++++- .../Pages/Home/MealStates/MovingToMouth.jsx | 25 +- .../MealStates/MovingToStagingLocation.jsx | 25 +- .../src/Pages/Home/MealStates/StowingArm.jsx | 25 +- feedingwebapp/src/ros/TestROSService.jsx | 3 + feedingwebapp/src/ros/ros_helpers.js | 11 +- 16 files changed, 681 insertions(+), 150 deletions(-) create mode 100644 feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveAbovePlate.py create mode 100644 feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveToDummy.py diff --git a/feeding_web_app_ros2_msgs/package.xml b/feeding_web_app_ros2_msgs/package.xml index 8a8bfc69..a9b2d50e 100644 --- a/feeding_web_app_ros2_msgs/package.xml +++ b/feeding_web_app_ros2_msgs/package.xml @@ -5,7 +5,7 @@ 0.0.0 Defines the message types used by `feeding_web_app_ros2_test`. Amal Nanavati - TODO: License declaration + BSD-3-Clause ament_cmake ament_lint_auto diff --git a/feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveAbovePlate.py b/feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveAbovePlate.py new file mode 100644 index 00000000..d778cc11 --- /dev/null +++ b/feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveAbovePlate.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +from ada_feeding_msgs.action import MoveTo +from feeding_web_app_ros2_test.MoveToDummy import MoveToDummy +import rclpy +from rclpy.executors import MultiThreadedExecutor + + +def main(args=None): + rclpy.init(args=args) + + move_above_plate = MoveToDummy("MoveAbovePlate", MoveTo) + + # Use a MultiThreadedExecutor to enable processing goals concurrently + executor = MultiThreadedExecutor() + + rclpy.spin(move_above_plate, executor=executor) + + +if __name__ == "__main__": + main() diff --git a/feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveToDummy.py b/feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveToDummy.py new file mode 100644 index 00000000..47e73223 --- /dev/null +++ b/feeding_web_app_ros2_test/feeding_web_app_ros2_test/MoveToDummy.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +import rclpy +from rclpy.action import ActionServer, CancelResponse, GoalResponse +from rclpy.node import Node +import threading +import time + + +class MoveToDummy(Node): + def __init__( + self, + name, + action_class, + send_feedback_hz=10, + dummy_plan_time=2.5, + dummy_motion_time=10.0, + ): + """ + Initialize the MoveToDummy action node. + + Parameters + ---------- + name: The name of the action server. + action_class: The action class to use for the action server. + send_feedback_hz: The target frequency at which to send feedback. + dummy_plan_time: How many seconds this dummy node should spend in planning. + dummy_motion_time: How many seconds this dummy node should spend in motion. + """ + super().__init__(name) + + self.send_feedback_hz = send_feedback_hz + self.dummy_plan_time = dummy_plan_time + self.dummy_motion_time = dummy_motion_time + self.action_class = action_class + + self.active_goal_request = None + + self._action_server = ActionServer( + self, + action_class, + name, + self.execute_callback, + goal_callback=self.goal_callback, + cancel_callback=self.cancel_callback, + ) + + def goal_callback(self, goal_request): + """ + Accept a goal if this action does not already have an active goal, + else reject. + + Parameters + ---------- + goal_request: The goal request message. + """ + self.get_logger().info("Received goal request") + if self.active_goal_request is None: + self.get_logger().info("Accepting goal request") + self.active_goal_request = goal_request + return GoalResponse.ACCEPT + self.get_logger().info("Rejecting goal request") + return GoalResponse.REJECT + + def cancel_callback(self, goal_handle): + """ + Always accept client requests to cancel the active goal. Note that this + function should not actually impelement the cancel; that is handled in + `execute_callback` + + Parameters + ---------- + goal_handle: The goal handle. + """ + self.get_logger().info("Received cancel request, accepting") + return CancelResponse.ACCEPT + + def plan(self, plan, success): + """ + A dummy thread for planning to the target position. This thread + will sleep for `self.dummy_plan_time` sec and then set the plan to None. + + Parameters + ---------- + plan: A mutable object, which will contain the plan once the thread has + finished. For this dummy thread, it contains None. + success: A mutable object, which will contain the success status once + the thread has finished. + """ + time.sleep(self.dummy_plan_time) + plan.append(None) + success[0] = True + + def move(self, plan, success): + """ + A dummy thread for moving the robot arm along the plan. This thread + will sleep for `self.dummy_motion_time` sec and then return success. + + Parameters + ---------- + plan: Contains the plan. + success: A mutable object, which will contain the success status once + the thread has finished. + """ + time.sleep(self.dummy_motion_time) + success[0] = True + + async def execute_callback(self, goal_handle): + """ + First, plan to the target position. Then, move to that position. + As a "dummy node," this specific node will spend `self.dummy_plan_time` + sec in planning and `self.dummy_motion_time` sec in motion. + + NOTE: In the actual (not dummy) implementation, we will be calling a + ROS action defined by MoveIt to do both planning and execution. Because + actions are non-blocking, we won't need to break off separate threads. + Using separate threads is an artiface of using time.sleep() in this + dummy implementation. + """ + self.get_logger().info("Executing goal...%s" % (goal_handle,)) + + # Load the feedback parameters + feedback_rate = self.create_rate(self.send_feedback_hz) + feedback_msg = self.action_class.Feedback() + + # Start the planning thread + plan = [] + plan_success = [False] + planning_thread = threading.Thread( + target=self.plan, args=(plan, plan_success), daemon=True + ) + planning_thread.start() + planning_start_time = self.get_clock().now() + is_planning = True + + # Create (but don't yet start) the motion thread + is_moving = False + motion_success = [False] + motion_thread = threading.Thread( + target=self.move, args=(plan, motion_success), daemon=True + ) + + # Monitor the planning and motion threads, and send feedback + while rclpy.ok() and (is_planning or is_moving): + # Check if there is a cancel request + if goal_handle.is_cancel_requested: + self.get_logger().info("Goal canceled") + goal_handle.canceled() + result = self.action_class.Result() + result.status = result.STATUS_CANCELED + self.active_goal_request = None # Clear the active goal + return result + + # Check if the planning thread has finished + if is_planning: + if not planning_thread.is_alive(): + is_planning = False + if plan_success[0]: # Plan succeeded + self.get_logger().info( + "Planning succeeded, proceeding to motion" + ) + # Start the motion thread + motion_thread.start() + motion_start_time = self.get_clock().now() + is_moving = True + continue + else: # Plan failed + self.get_logger().info("Planning failed, aborting") + # Abort the goal + goal_handle.abort() + result = self.action_class.Result() + result.status = result.STATUS_PLANNING_FAILED + self.active_goal_request = None # Clear the active goal + return result + + # Check if the motion thread has finished + if is_moving: + if not motion_thread.is_alive(): + is_moving = False + if motion_success[0]: + self.get_logger().info("Motion succeeded, returning") + # Succeed the goal + goal_handle.succeed() + result = self.action_class.Result() + result.status = result.STATUS_SUCCESS + self.active_goal_request = None # Clear the active goal + return result + else: + self.get_logger().info("Motion failed, aborting") + # Abort the goal + goal_handle.abort() + result = self.action_class.Result() + result.status = result.STATUS_MOTION_FAILED + self.active_goal_request = None # Clear the active goal + return result + + # Send feedback + feedback_msg.is_planning = is_planning + if is_planning: + feedback_msg.planning_time = ( + self.get_clock().now() - planning_start_time + ).to_msg() + elif is_moving: + # TODO: In the actual (not dummy) implementation, this should + # return the distance (not time) the robot has yet to move. + feedback_msg.motion_initial_distance = self.dummy_motion_time + elapsed_time = self.get_clock().now() - motion_start_time + elapsed_time_float = elapsed_time.nanoseconds / 1.0e9 + feedback_msg.motion_curr_distance = ( + self.dummy_motion_time - elapsed_time_float + ) + self.get_logger().info("Feedback: %s" % feedback_msg) + goal_handle.publish_feedback(feedback_msg) + + # Sleep for the specified feedback rate + feedback_rate.sleep() + + # If we get here, something went wrong + self.get_logger().info("Unknown error, aborting") + goal_handle.abort() + result = self.action_class.Result() + result.status = result.STATUS_UNKNOWN + self.active_goal_request = None # Clear the active goal + return result diff --git a/feeding_web_app_ros2_test/package.xml b/feeding_web_app_ros2_test/package.xml index 0baccd1f..f35640d8 100644 --- a/feeding_web_app_ros2_test/package.xml +++ b/feeding_web_app_ros2_test/package.xml @@ -5,7 +5,7 @@ 0.0.0 A minimal ROS publisher, subscriber, service, and action to use to test the web app. Amal Nanavati - TODO: License declaration + BSD-3-Clause ament_copyright ament_flake8 diff --git a/feeding_web_app_ros2_test/setup.py b/feeding_web_app_ros2_test/setup.py index 92e2b7af..827bdcfc 100644 --- a/feeding_web_app_ros2_test/setup.py +++ b/feeding_web_app_ros2_test/setup.py @@ -15,10 +15,13 @@ maintainer="Amal Nanavati", maintainer_email="amaln@cs.washington.edu", description="A minimal ROS publisher, subscriber, service, and action to use to test the web app.", - license="TODO: License declaration", + license="BSD-3-Clause", tests_require=["pytest"], entry_points={ "console_scripts": [ + # Scripts for the main app + "MoveAbovePlate = feeding_web_app_ros2_test.MoveAbovePlate:main", + # Scripts for the "TestROS" component "listener = feeding_web_app_ros2_test.subscriber:main", "reverse_string = feeding_web_app_ros2_test.reverse_string_service:main", "sort_by_character_frequency = feeding_web_app_ros2_test.sort_by_character_frequency_action:main", diff --git a/feedingwebapp/README.md b/feedingwebapp/README.md index 3a1c2c4e..2202337b 100644 --- a/feedingwebapp/README.md +++ b/feedingwebapp/README.md @@ -14,6 +14,7 @@ The overall user flow for this robot can be seen below. - [Node.js](https://nodejs.org/en/download/package-manager) - [ROS2 Humble](https://docs.ros.org/en/humble/Installation.html) - [PRL fork of rosbridge_suite](https://github.com/personalrobotics/rosbridge_suite). This fork enables rosbridge_suite to communicate with ROS2 actions. +- [ada_feeding (branch: ros2-devel)](https://github.com/personalrobotics/ada_feeding/tree/ros2-devel). ## Getting Started in Computer @@ -31,14 +32,29 @@ The overall user flow for this robot can be seen below. - Note that if you're not running the robot code alongside the app, set [`debug = true` in `App.jsx`](https://github.com/personalrobotics/feeding_web_interface/tree/main/feedingwebapp/src/App.jsx#L17) to be able to move past screens where the app is waiting on the robot. Since the robot is not yet connected, the default is `debug = true` 3. Use a web browser to navigate to `localhost:3000` to see the application. +#### Launching Dummy Nodes +This repository includes several dummy nodes that match the interface that the robot nodes will use. By running the dummy nodes alongside the app, we can test the app's communication with the robot even without actual robot code running. + +The below instructions are for `MoveAbovePlate`; we will add to the instructions as more dummy nodes get implemented. +1. Navigate to your ROS2 workspace: `cd {path/to/your/ros2/workspace}` +2. Build your workspace: `colcon build` +3. Launch rosbridge: `source install/setup.bash; ros2 launch rosbridge_server rosbridge_websocket_launch.xml` +4. In another terminal, run the MoveAbovePlate action: `source install/setup.bash; ros2 run feeding_web_app_ros2_test MoveAbovePlate` +5. In another terminal, navigate to the web app folder: `cd {path/to/feeding_web_interface}/feedingwebapp` +6. Start the app: `npm start` +7. Use a web browser to navigate to `localhost:3000`. + +You should now see that the web browser is connected to ROS. Further, you should see that when the `MoveAbovePlate` page starts, it should call the action (exactly once), and render feedback. "Pause" should cancel the action, and "Resume" should re-call it. Refreshing the page should cancel the action. When the action returns success, the app should automatically transition to the next page. + ### Usage (Test ROS) There is a special page in the app intended for developers to: (a) test that their setup of the app and ROS2 enables the two to communicate as expected; and (b) gain familiarity with the library of ROS helper functions we use in the web app (see [TechDocumentation.md](https://github.com/personalrobotics/feeding_web_interface/tree/main/feedingwebapp/TechDocumentation.md)). Below are instructions to use this page: 1. Navigate to your ROS2 workspace: `cd {path/to/your/ros2/workspace}` 2. Build your workspace: `colcon build` -3. Launch rosbridge: `ros2 launch rosbridge_server rosbridge_websocket_launch.xml` -4. In another terminal, navigate to the web app folder: `cd {path/to/feeding_web_interface}/feedingwebapp` -5. Start the app: `npm start` -6. Use a web browser to navigate to `localhost:3000/test_ros`. +3. Source your workspace: `source install/setup.bash` +4. Launch rosbridge: `ros2 launch rosbridge_server rosbridge_websocket_launch.xml` +5. In another terminal, navigate to the web app folder: `cd {path/to/feeding_web_interface}/feedingwebapp` +6. Start the app: `npm start` +7. Use a web browser to navigate to `localhost:3000/test_ros`. The following are checks to ensure the app is interacting with ROS as expected. If any of them fails, you'll have to do additional troubleshooting to get the web app and ROS2 smoothly communicating with each other. 1. First, check if the page says `Connected` at the top of the screen. If not, the web app is not communicating with ROS2. diff --git a/feedingwebapp/TechDocumentation.md b/feedingwebapp/TechDocumentation.md index 15fc852d..acdfb374 100644 --- a/feedingwebapp/TechDocumentation.md +++ b/feedingwebapp/TechDocumentation.md @@ -19,6 +19,14 @@ All functions to interact with ROS are defined in [`ros_helpers.jsx`](https://gi Although the web app's settings menu seem parameter-esque it is not a good idea to update settings by getting/setting ROS parameters. That is because ROS2 parameters are owned by individual nodes, and do not persist beyond the lifetime of a node. Further, we likely want to run downstream code after updating any setting (e.g., writing it to a file as backup), which cannot easily be done when setting ROS parameters. Therefore, **updating settings should be done by ROS service calls, not by getting/setting ROS parameters**. Hence, `ros_helpers.jsx` does not even implement parameter getting/setting. +### Using ROS Services, Actions, etc. Within React + +React, by default, will re-render a component any time local state changes. In other words, it will re-run all code in the function that defines that component. However, this doesn't work well for ROS Services, Actions, etc. because: (a) we don't want to keep re-calling the service/action each time the UI re-renders; and (b) we don't want to keep re-creating a service/action client each time the UI re-renders. Therefore, it is crucial to use React Hooks intentionally and to double and triple check that the service/action is only getting once, to avoid bugs. + +These resources that can help you understand React Hooks in general: +- https://medium.com/@guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928 +- https://stackoverflow.com/a/69264685 + ## Frequently Asked Questions (FAQ) Q: Why are we using global state as opposed to a [`Router`](https://www.w3schools.com/react/react_router.asp) to decide which app components to render? diff --git a/feedingwebapp/src/Pages/Constants.js b/feedingwebapp/src/Pages/Constants.js index 586bf6ff..367e3d20 100644 --- a/feedingwebapp/src/Pages/Constants.js +++ b/feedingwebapp/src/Pages/Constants.js @@ -1,4 +1,5 @@ import { MEAL_STATE } from './GlobalState' + // The RealSense's default video stream is 640x480 export const REALSENSE_WIDTH = 640 export const REALSENSE_HEIGHT = 480 @@ -21,3 +22,28 @@ FOOTER_STATE_ICON_DICT[MEAL_STATE.R_MovingToStagingLocation] = '/robot_state_img FOOTER_STATE_ICON_DICT[MEAL_STATE.R_MovingToMouth] = '/robot_state_imgs/move_to_mouth_position.svg' FOOTER_STATE_ICON_DICT[MEAL_STATE.R_StowingArm] = '/robot_state_imgs/stowing_arm_position.svg' export { FOOTER_STATE_ICON_DICT } + +// For states that call ROS actions, this dictionary contains +// the action name and the message type +let ROS_ACTIONS_NAMES = {} +ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingAbovePlate] = { + actionName: 'MoveAbovePlate', + messageType: 'ada_feeding_msgs/action/MoveTo' +} +export { ROS_ACTIONS_NAMES } + +// The meaning of the status that motion actions return in their results. +// These should match the action definition(s) +export const MOTION_STATUS_SUCCESS = 0 +export const MOTION_STATUS_PLANNING_FAILED = 1 +export const MOTION_STATUS_MOTION_FAILED = 2 +export const MOTION_STATUS_CANCELED = 3 +export const MOTION_STATUS_UNKNOWN = 99 + +// The meaning of ROS Action statuses. +// https://docs.ros2.org/latest/api/rclpy/api/actions.html#rclpy.action.server.GoalEvent +export const ROS_ACTION_STATUS_EXECUTE = '1' +export const ROS_ACTION_STATUS_CANCEL_GOAL = '2' +export const ROS_ACTION_STATUS_SUCCEED = '3' +export const ROS_ACTION_STATUS_ABORT = '4' +export const ROS_ACTION_STATUS_CANCELED = '5' diff --git a/feedingwebapp/src/Pages/Footer/Footer.jsx b/feedingwebapp/src/Pages/Footer/Footer.jsx index 7b96e159..c98052e5 100644 --- a/feedingwebapp/src/Pages/Footer/Footer.jsx +++ b/feedingwebapp/src/Pages/Footer/Footer.jsx @@ -1,108 +1,72 @@ // React imports -import React, { useState } from 'react' +import React, { useCallback } from 'react' import { MDBFooter } from 'mdb-react-ui-kit' import Button from 'react-bootstrap/Button' import { View } from 'react-native' import Row from 'react-bootstrap/Row' +// PropTypes is used to validate that the used props are in fact passed to this +// Component +import PropTypes from 'prop-types' // Local imports import { FOOTER_STATE_ICON_DICT } from '../Constants' -import { useGlobalState, MEAL_STATE } from '../GlobalState' +import { useGlobalState } from '../GlobalState' /** * The Footer shows a pause button. When users click it, the app tells the robot * to immediately pause and displays a back button that allows them to return to * previous state and a resume button that allows them to resume current state. */ -const Footer = () => { - // Set the current meal state - const setMealState = useGlobalState((state) => state.setMealState) +const Footer = (props) => { // Get the current meal state const mealState = useGlobalState((state) => state.mealState) - /** - * Regardless of the state, the back button should revert to MoveAbovePlate. - * - BiteAcquisition: In this case, pressing "back" should let the user - * reselect the bite, which requires the robot to move above plate. - * - MoveToStagingLocation: In this case, pressing "back" should move the - * robot back to the plate. Although the user may not always want to - * reselect the bite, from `BiteSelection` they have the option to skip - * BiteAcquisition and move straight to staging location (when they are ready). - * - MoveToMouth: Although in some cases the user may want "back" to move to - * the staging location, since we will be removing the staging location - * (Issue #45) it makes most sense to move the robot back to the plate. - * - StowingArm: In this case, if the user presses back they likely want to - * eat another bite, hence moving above the plate makes sense. - * - MovingAbovePlate: Although the user may want to press "back" to move - * the robot to the staging location, they can also go forward to - * BiteSelection and then move the robot to the staging location. - * Hence, in this case we don't have a "back" button. - */ - // A local variable for storing back icon image - var backIcon = FOOTER_STATE_ICON_DICT[MEAL_STATE.R_MovingAbovePlate] - // A local variable for storing current state icon image - var resumeIcon = FOOTER_STATE_ICON_DICT[mealState] - // Local state variable to track of visibility of pause button - const [pauseButtonVisible, setPauseButtonVisible] = useState(true) - // Local state variable to track of visibility of back button - const [backButtonVisible, setBackButtonVisible] = useState(false) - // Local state variable to track of visibility of resume button - const [resumeButtonVisible, setResumeButtonVisible] = useState(false) - // width of Back and Resume buttons + + // Icons and other parameters for the footer buttons + let pauseIcon = '/robot_state_imgs/pause_button_icon.svg' + let backIcon = props.backMealState ? FOOTER_STATE_ICON_DICT[props.backMealState] : '' + let resumeIcon = FOOTER_STATE_ICON_DICT[mealState] + let phantomButtonIcon = '/robot_state_imgs/phantom_view_image.svg' + // Width of Back and Resume buttons let backResumeButtonWidth = '150px' - // height of all Footer buttons + // Height of all Footer buttons let footerButtonHight = '100px' - /** - * When the pause button is clicked, show back and/or resume buttons. + * When the pause button is clicked, execute the callback and display the + * back and resume buttons. */ - function pauseButtonClicked() { - /** We call setPauseButtonVisible, setBackButtonVisible, and setResumeButtonVisible - * with new values to make pause button invisible and resume and/or back button visible. - * React will re-render the Footer component. - */ - setPauseButtonVisible(false) - if (mealState === MEAL_STATE.R_BiteAcquisition) { - setBackButtonVisible(true) - } else if (mealState === MEAL_STATE.R_MovingAbovePlate) { - setResumeButtonVisible(true) - } else { - setBackButtonVisible(true) - setResumeButtonVisible(true) - } - } + const pauseClicked = useCallback(() => { + props.pauseCallback() + props.setPaused(true) + }, []) /** - * When the resume button is clicked, continue with the current meal state. + * When the resume button is clicked, execute the callback and display the + * pause button. */ - function resumeButtonClicked() { - /** We call setPauseButtonVisible, setBackButtonVisible, and setResumeButtonVisible - * with new values to make pause button visible again. - * React will re-render the Footer component. - */ - setPauseButtonVisible(true) - setBackButtonVisible(false) - setResumeButtonVisible(false) - } + const resumeClicked = useCallback(() => { + if (props.resumeCallback) { + props.resumeCallback() + } + props.setPaused(false) + }, []) /** - * When the back button is clicked, go back to previous state. + * When the back button is clicked, execute the callback and display the pause + * button. */ - function backButtonClicked() { - // Set meal state to move above plate - setMealState(MEAL_STATE.R_MovingAbovePlate) - /** We call setPauseButtonVisible, setBackButtonVisible, and setResumeButtonVisible - * with new values to make pause button visible again. - * React will re-render the Footer component. - */ - resumeButtonClicked() - } + const backClicked = useCallback(() => { + if (props.backCallback) { + props.backCallback() + } + props.setPaused(false) + }, []) /** * Get the pause text and button to render in footer. * * @returns {JSX.Element} the pause text and button */ - let pauseTextAndButton = function () { + const renderPauseButton = useCallback(() => { return ( <> @@ -112,27 +76,22 @@ const Footer = () => { {/* Icon to pause */} ) - } + }, [pauseClicked]) /** * Get the back text and button to render in footer. * * @returns {JSX.Element} the back text and button */ - let backTextAndButton = function () { + const renderBackButton = useCallback(() => { return ( <>

@@ -141,21 +100,21 @@ const Footer = () => { {/* Icon to move to previous state */} ) - } + }, [backClicked]) /** * Get the resume text and button to render in footer. * * @returns {JSX.Element} the resume text and button */ - let resumeTextAndButton = function () { + const renderResumeButton = useCallback(() => { return ( <>

{ {/* Icon to resume current state */} ) - } + }, [resumeClicked]) /** - * Get the phantom view to render in footer. + * Get the phantom view to render in footer. This is used as a placeholder + * when the back button or resume button are disabled. * * @returns {JSX.Element} the phantom view */ - let phantomView = function () { + let renderPhantomButton = function () { return ( <> ) @@ -210,41 +165,29 @@ const Footer = () => { // Render the component return ( <> - {/** - * The footer shows a pause button first. A resume button and/or - * a back button are shown when the pause button is clicked. - * - * - If the pause button is visible, regardless of the meal state - * show a pause button taking the whole width of the footer screen. - * - * - In Move Above Plate state, if pause button is not visible, - * only show the resume button and no back button, since the user - * can go "forward" to any other state as opposed to going back. - * - * - In Bite Acquisition state, if pause button is not visible, - * only show the back button and no resume button, since the user - * can continue to acquiring bite again after moving above plate, - * but if they resume this state after aquiring bite, bite selection - * mask may no longer work because the food may have shifted - * - * - For any other meal state (e.g., MoveToStagingLocation, MoveToMouth, - * and StowingArm), if the pause button is not visible, then the footer - * shows both the back and resume buttons - */}

- {pauseButtonVisible ? ( - pauseTextAndButton() - ) : ( + {props.paused ? ( - {backButtonVisible ? backTextAndButton() : phantomView()} - {resumeButtonVisible ? resumeTextAndButton() : phantomView()} + {props.backCallback && props.backMealState ? renderBackButton() : renderPhantomButton()} + {props.resumeCallback ? renderResumeButton() : renderPhantomButton()} + ) : ( + renderPauseButton() )}
) } +Footer.propTypes = { + paused: PropTypes.bool.isRequired, + setPaused: PropTypes.func.isRequired, + pauseCallback: PropTypes.func.isRequired, + // If any of the below three are null, the Footer won't render that button + resumeCallback: PropTypes.func, + backCallback: PropTypes.func, + backMealState: PropTypes.string +} export default Footer diff --git a/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisition.jsx b/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisition.jsx index 852674f2..bc0b50c4 100644 --- a/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisition.jsx +++ b/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisition.jsx @@ -1,5 +1,5 @@ // React Imports -import React from 'react' +import React, { useState } from 'react' import Button from 'react-bootstrap/Button' // PropTypes is used to validate that the used props are in fact passed to this // Component @@ -19,6 +19,9 @@ import { useGlobalState, MEAL_STATE } from '../../GlobalState' * @params {object} props - contains any properties passed to this Component */ const BiteAcquisition = (props) => { + // Create a local state variable for whether the robot is paused. + const [paused, setPaused] = useState(false) + // Get the relevant global variables const setMealState = useGlobalState((state) => state.setMealState) const desiredFoodItem = useGlobalState((state) => state.desiredFoodItem) @@ -31,6 +34,15 @@ const BiteAcquisition = (props) => { setMealState(MEAL_STATE.U_BiteAcquisitionCheck) } + /** + * Callback function for when the back button is clicked. + */ + const backMealState = MEAL_STATE.R_MovingAbovePlate + function backCallback() { + console.log('Back Clicked') + setMealState(backMealState) + } + // Render the component return ( <> @@ -50,14 +62,23 @@ const BiteAcquisition = (props) => { {/** - * Display the footer with the Pause button. + * Display the footer with the Pause button. BiteAcquisition has no resume + * button, because the selected food mask may no longer be usable (e.g., + * if the robot moved the food items) */} -