diff --git a/feedingwebapp/src/Pages/GlobalState.jsx b/feedingwebapp/src/Pages/GlobalState.jsx index 0a3e03e..c6bebc8 100644 --- a/feedingwebapp/src/Pages/GlobalState.jsx +++ b/feedingwebapp/src/Pages/GlobalState.jsx @@ -130,8 +130,8 @@ export const useGlobalState = create( // message received from the face detection node where a // face was detected and within the distance bounds of the camera. moveToMouthActionGoal: null, - // Last RobotMotion action response - lastMotionActionResponse: null, + // Last RobotMotion action feedback message + lastMotionActionFeedback: null, // Whether or not the currently-executing robot motion was paused by the user. // NOTE: `paused` may no longer need to be in global state now that we have // the `inNonMovingState` flag. @@ -218,9 +218,9 @@ export const useGlobalState = create( set(() => ({ biteAcquisitionActionGoal: biteAcquisitionActionGoal })), - setLastMotionActionResponse: (lastMotionActionResponse) => + setLastMotionActionFeedback: (lastMotionActionFeedback) => set(() => ({ - lastMotionActionResponse: lastMotionActionResponse + lastMotionActionFeedback: lastMotionActionFeedback })), setMoveToMouthActionGoal: (moveToMouthActionGoal) => set(() => { diff --git a/feedingwebapp/src/Pages/Home/Home.jsx b/feedingwebapp/src/Pages/Home/Home.jsx index 8642481..ca6dc21 100644 --- a/feedingwebapp/src/Pages/Home/Home.jsx +++ b/feedingwebapp/src/Pages/Home/Home.jsx @@ -1,10 +1,12 @@ // React imports -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useEffect, useMemo, useRef } from 'react' import PropTypes from 'prop-types' +import { toast } from 'react-toastify' import { View } from 'react-native' // Local imports import './Home.css' +import { createROSService, createROSServiceRequest, useROS } from '../../ros/ros_helpers' import { useGlobalState, MEAL_STATE } from '../GlobalState' import BiteAcquisitionCheck from './MealStates/BiteAcquisitionCheck' import BiteDone from './MealStates/BiteDone' @@ -13,7 +15,13 @@ import DetectingFace from './MealStates/DetectingFace' import PostMeal from './MealStates/PostMeal' import PreMeal from './MealStates/PreMeal' import RobotMotion from './MealStates/RobotMotion' -import { getRobotMotionText, TIME_TO_RESET_MS } from '../Constants' +import { + ACQUISITION_REPORT_SERVICE_NAME, + ACQUISITION_REPORT_SERVICE_TYPE, + getRobotMotionText, + REGULAR_CONTAINER_ID, + TIME_TO_RESET_MS +} from '../Constants' /** * The Home component displays the state of the meal, solicits user input as @@ -78,6 +86,47 @@ function Home(props) { const moveToMouthActionInput = useMemo(() => moveToMouthActionGoal, [moveToMouthActionGoal]) const moveToStowPositionActionInput = useMemo(() => ({}), []) + /** + * Create callbacks for acquisition success and failure. This is done here because these + * callbacks can be called during BiteAcquisition or the BiteAcquisitionCheck. + */ + const lastMotionActionFeedback = useGlobalState((state) => state.lastMotionActionFeedback) + const ros = useRef(useROS().ros) + let acquisitionReportService = useRef(createROSService(ros.current, ACQUISITION_REPORT_SERVICE_NAME, ACQUISITION_REPORT_SERVICE_TYPE)) + let acquisitionResponse = useCallback( + (success) => { + if (!lastMotionActionFeedback.action_info_populated) { + console.info('Cannot report acquisition success or failure without action_info_populated.') + return + } + let msg, loss + if (success) { + msg = 'Reporting Food Acquisition Success!' + loss = 0.0 + } else { + msg = 'Reporting Food Acquisition Failure.' + loss = 1.0 + } + // NOTE: This uses the ToastContainer in Header + console.log(msg) + toast.info(msg, { + containerId: REGULAR_CONTAINER_ID, + toastId: msg + }) + // Create a service request + let request = createROSServiceRequest({ + loss: loss, + action_index: lastMotionActionFeedback.action_index, + posthoc: lastMotionActionFeedback.posthoc, + id: lastMotionActionFeedback.selection_id + }) + // Call the service + let service = acquisitionReportService.current + service.callService(request, (response) => console.log('Got acquisition report response', response)) + }, + [lastMotionActionFeedback] + ) + /** * Determines what screen to render based on the meal state. */ @@ -119,7 +168,8 @@ function Home(props) { let nextMealState = MEAL_STATE.U_BiteAcquisitionCheck let backMealState = MEAL_STATE.R_MovingAbovePlate // TODO: Add an icon for this errorMealState! - let errorMealState = MEAL_STATE.R_MovingToRestingPosition + let errorMealState = MEAL_STATE.R_MovingToStagingConfiguration + let errorCallback = () => acquisitionResponse(true) // Success if the user skips acquisition let errorMealStateDescription = 'Skip Acquisition' return ( ) @@ -153,7 +204,7 @@ function Home(props) { ) } case MEAL_STATE.U_BiteAcquisitionCheck: { - return + return } case MEAL_STATE.R_MovingToStagingConfiguration: { /** @@ -257,6 +308,7 @@ function Home(props) { props.debug, props.webrtcURL, biteAcquisitionActionInput, + acquisitionResponse, mostRecentBiteDoneResponse, moveAbovePlateActionInput, moveToMouthActionInput, diff --git a/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisitionCheck.jsx b/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisitionCheck.jsx index 38c2cb5..ab01cf4 100644 --- a/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisitionCheck.jsx +++ b/feedingwebapp/src/Pages/Home/MealStates/BiteAcquisitionCheck.jsx @@ -2,28 +2,22 @@ import React, { useCallback, useEffect, useState, useRef } from 'react' import Button from 'react-bootstrap/Button' import { useMediaQuery } from 'react-responsive' -import { toast } from 'react-toastify' import { View } from 'react-native' +// PropTypes is used to validate that the used props are in fact passed to this Component +import PropTypes from 'prop-types' // Local Imports import '../Home.css' import { useGlobalState, MEAL_STATE } from '../../GlobalState' import { MOVING_STATE_ICON_DICT } from '../../Constants' import { useROS, createROSService, createROSServiceRequest, subscribeToROSTopic, unsubscribeFromROSTopic } from '../../../ros/ros_helpers' -import { - ACQUISITION_REPORT_SERVICE_NAME, - ACQUISITION_REPORT_SERVICE_TYPE, - FOOD_ON_FORK_DETECTION_TOPIC, - FOOD_ON_FORK_DETECTION_TOPIC_MSG, - REGULAR_CONTAINER_ID, - ROS_SERVICE_NAMES -} from '../../Constants' +import { FOOD_ON_FORK_DETECTION_TOPIC, FOOD_ON_FORK_DETECTION_TOPIC_MSG, ROS_SERVICE_NAMES } from '../../Constants' /** * The BiteAcquisitionCheck component appears after the robot has attempted to * acquire a bite, and asks the user whether it succeeded at acquiring the bite. */ -const BiteAcquisitionCheck = () => { +const BiteAcquisitionCheck = (props) => { // Store the remining time before auto-continuing const [remainingSeconds, setRemainingSeconds] = useState(null) // Get the relevant global variables @@ -50,17 +44,11 @@ const BiteAcquisitionCheck = () => { let iconWidth = isPortrait ? '28vh' : '28vw' let iconHeight = isPortrait ? '18vh' : '18vw' - // Configure AcquisitionReport service - const lastMotionActionResponse = useGlobalState((state) => state.lastMotionActionResponse) /** * Connect to ROS, if not already connected. Put this in useRef to avoid * re-connecting upon re-renders. */ const ros = useRef(useROS().ros) - /** - * Create the ROS Service Client for reporting success/failure - */ - let acquisitionReportService = useRef(createROSService(ros.current, ACQUISITION_REPORT_SERVICE_NAME, ACQUISITION_REPORT_SERVICE_TYPE)) /** * Create the ROS Service. This is created in local state to avoid re-creating * it upon every re-render. @@ -73,48 +61,20 @@ const BiteAcquisitionCheck = () => { * succeeded. */ const acquisitionSuccess = useCallback(() => { - console.log('acquisitionSuccess') - // NOTE: This uses the ToastContainer in Header - toast.info('Reporting Food Acquisition Success!', { - containerId: REGULAR_CONTAINER_ID, - toastId: 'foodAcquisitionSuccess' - }) - // Create a service request - let request = createROSServiceRequest({ - loss: 0.0, - action_index: lastMotionActionResponse.action_index, - posthoc: lastMotionActionResponse.posthoc, - id: lastMotionActionResponse.selection_id - }) - // Call the service - let service = acquisitionReportService.current - service.callService(request, (response) => console.log('Got acquisition report response', response)) + let acquisitionResponse = props.acquisitionResponse + acquisitionResponse(true) setMealState(MEAL_STATE.R_MovingToStagingConfiguration) - }, [lastMotionActionResponse, setMealState]) + }, [props.acquisitionResponse, setMealState]) /** * Callback function for when the user indicates that the bite acquisition * failed. */ const acquisitionFailure = useCallback(() => { - console.log('acquisitionFailure') - // NOTE: This uses the ToastContainer in Header - toast.info('Reporting Food Acquisition Failure.', { - containerId: REGULAR_CONTAINER_ID, - toastId: 'foodAcquisitionFailure' - }) - // Create a service request - let request = createROSServiceRequest({ - loss: 1.0, - action_index: lastMotionActionResponse.action_index, - posthoc: lastMotionActionResponse.posthoc, - id: lastMotionActionResponse.selection_id - }) - // Call the service - let service = acquisitionReportService.current - service.callService(request, (response) => console.log('Got acquisition report response', response)) + let acquisitionResponse = props.acquisitionResponse + acquisitionResponse(false) setMealState(MEAL_STATE.R_MovingAbovePlate) - }, [lastMotionActionResponse, setMealState]) + }, [props.acquisitionResponse, setMealState]) /* * Create refs to store the interval for the food-on-fork detection timers. @@ -429,4 +389,11 @@ const BiteAcquisitionCheck = () => { return <>{fullPageView()} } +BiteAcquisitionCheck.propTypes = { + debug: PropTypes.bool, + // A function that takes a boolean indicating whether the robot succeeded at acquiring the bite, + // and processes the response. Note that it does not transition to the next state. + acquisitionResponse: PropTypes.func.isRequired +} + export default BiteAcquisitionCheck diff --git a/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx b/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx index 85e9d7e..9954b7a 100644 --- a/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx +++ b/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx @@ -62,8 +62,8 @@ const RobotMotion = (props) => { const paused = useGlobalState((state) => state.paused) const setPaused = useGlobalState((state) => state.setPaused) - // Setter for last motion action response - const setLastMotionActionResponse = useGlobalState((state) => state.setLastMotionActionResponse) + // Setter for last motion action feedback msg + const setLastMotionActionFeedback = useGlobalState((state) => state.setLastMotionActionFeedback) /** * Connect to ROS, if not already connected. Put this in useRef to avoid @@ -115,23 +115,27 @@ const RobotMotion = (props) => { const feedbackCallback = useCallback( (feedbackMsg) => { console.log('Got feedback message', feedbackMsg) + setLastMotionActionFeedback(feedbackMsg.values.feedback) setActionStatus({ actionStatus: ROS_ACTION_STATUS_EXECUTE, feedback: feedbackMsg.values.feedback }) }, - [setActionStatus] + [setActionStatus, setLastMotionActionFeedback] ) /** * Callback function to change the meal state. */ const changeMealState = useCallback( - (nextMealState, msg = null) => { + (nextMealState, msg = null, callback = null) => { if (msg) { console.log(msg) } setPaused(false) + if (callback) { + callback() + } let setMealState = props.setMealState setMealState(nextMealState) }, @@ -164,7 +168,6 @@ const RobotMotion = (props) => { setActionStatus({ actionStatus: ROS_ACTION_STATUS_SUCCEED }) - setLastMotionActionResponse(response.values) robotMotionDone() } else { if ( @@ -185,7 +188,7 @@ const RobotMotion = (props) => { } } }, - [setLastMotionActionResponse, setActionStatus, setPaused, robotMotionDone] + [setActionStatus, setPaused, robotMotionDone] ) /** @@ -327,7 +330,7 @@ const RobotMotion = (props) => { variant='warning' className='mx-2 btn-huge' size='lg' - onClick={() => changeMealState(props.errorMealState, 'errorMealState')} + onClick={() => changeMealState(props.errorMealState, 'errorMealState', props.errorCallback)} style={{ width: '90%', height: '20%' @@ -363,6 +366,7 @@ const RobotMotion = (props) => { props.waitingText, props.allowRetry, props.errorMealState, + props.errorCallback, props.errorMealStateDescription, motionTextFontSize, waitingTextFontSize, @@ -501,6 +505,7 @@ RobotMotion.propTypes = { allowRetry: PropTypes.bool, // If error, show the user the option to transition to this meal state errorMealState: PropTypes.string, + errorCallback: PropTypes.func, errorMealStateDescription: PropTypes.string } diff --git a/feedingwebapp/src/Pages/Settings/BiteTransfer.jsx b/feedingwebapp/src/Pages/Settings/BiteTransfer.jsx index bcfba22..b14c534 100644 --- a/feedingwebapp/src/Pages/Settings/BiteTransfer.jsx +++ b/feedingwebapp/src/Pages/Settings/BiteTransfer.jsx @@ -64,7 +64,7 @@ const BiteTransfer = (props) => { // Indicator of how to arrange screen elements based on orientation let dimension = isPortrait ? 'column' : 'row' // Rendering variables - let textFontSize = '3.5vh' + let textFontSize = '3.0vh' // Get min and max distance to mouth const minDistanceToMouth = 1 // cm @@ -227,9 +227,9 @@ const BiteTransfer = (props) => { const speedParameterIdsAndDescriptions = useMemo( () => [ [moveToMouthSpeedId, 'Approach Speed (cm/s)'], - [moveToMouthSpeedNearMouthId, 'Approach Speed Near Mouth (cm/s)'], + [moveToMouthSpeedNearMouthId, 'Approach Near Mouth (cm/s)'], [moveFromMouthSpeedId, 'Retreat Speed (cm/s)'], - [moveFromMouthSpeedNearMouthId, 'Retreat Speed Near Mouth (cm/s)'] + [moveFromMouthSpeedNearMouthId, 'Retreat Near Mouth (cm/s)'] ], [moveToMouthSpeedId, moveToMouthSpeedNearMouthId, moveFromMouthSpeedId, moveFromMouthSpeedNearMouthId] ) @@ -264,7 +264,7 @@ const BiteTransfer = (props) => { > { { Move To Mouth - + /> */} { Move From Mouth - + /> */} ) diff --git a/feedingwebapp/src/ros/ros_helpers.js b/feedingwebapp/src/ros/ros_helpers.js index 037bc2b..02fc742 100644 --- a/feedingwebapp/src/ros/ros_helpers.js +++ b/feedingwebapp/src/ros/ros_helpers.js @@ -157,6 +157,7 @@ export function callROSAction(actionClient, goal, feedbackCallback, resultCallba // server's callback list to prevent multiple callbacks from being executed in // further calls. actionClient.createClient(goal, resultCallback, feedbackCallback) + console.log('Called ROS Action') } /**