Skip to content

Commit

Permalink
Allow users to customize the distance to mouth during bite transfer (#…
Browse files Browse the repository at this point in the history
…113)

* [WIP] Implemented getting/setting parameter

TODO:
- Implement "Try It" along with error handling
- Test it

* Separated DetectingFace into two components for easier reuse

* Generalized RobotMotion to be called within Settings

* Remove dependency on generic props

* Finished implementing MVP bite transfer customizatipn

* Clip allowable distances to mouth

* Finalized it in sim

* Removed combo MoveFromMouthTo..., made 'Done' restore pre-Settings robot arm configuration

* Small fixes from sim testing

* Formatting
  • Loading branch information
amalnanavati authored Jan 16, 2024
1 parent 50b8c6a commit 3006f62
Show file tree
Hide file tree
Showing 18 changed files with 2,975 additions and 486 deletions.
1,874 changes: 1,828 additions & 46 deletions feedingwebapp/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions feedingwebapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fluentui/react-components": "^9.44.1",
"@mapbox/node-pre-gyp": "^1.0.11",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.2.0",
Expand Down
20 changes: 7 additions & 13 deletions feedingwebapp/src/Pages/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@ export const TIME_TO_RESET_MS = 3600000 // 1 hour in milliseconds
*/
let MOVING_STATE_ICON_DICT = {}
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingAbovePlate] = '/robot_state_imgs/move_above_plate_position.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToAbovePlate] = '/robot_state_imgs/move_above_plate_position.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToRestingPosition] = '/robot_state_imgs/move_to_resting_position.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToRestingPosition] = '/robot_state_imgs/move_to_resting_position.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToStagingConfiguration] = '/robot_state_imgs/move_to_staging_configuration.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToStagingConfiguration] = '/robot_state_imgs/move_to_staging_configuration.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouth] = '/robot_state_imgs/move_to_staging_configuration.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToMouth] = '/robot_state_imgs/move_to_mouth_position.svg'
MOVING_STATE_ICON_DICT[MEAL_STATE.R_StowingArm] = '/robot_state_imgs/stowing_arm_position.svg'
export { MOVING_STATE_ICON_DICT }
Expand Down Expand Up @@ -69,10 +67,6 @@ ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingAbovePlate] = {
actionName: 'MoveAbovePlate',
messageType: 'ada_feeding_msgs/action/MoveTo'
}
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToAbovePlate] = {
actionName: 'MoveFromMouthToAbovePlate',
messageType: 'ada_feeding_msgs/action/MoveTo'
}
ROS_ACTIONS_NAMES[MEAL_STATE.U_BiteSelection] = {
actionName: 'SegmentFromPoint',
messageType: 'ada_feeding_msgs/action/SegmentFromPoint'
Expand All @@ -85,16 +79,12 @@ ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToRestingPosition] = {
actionName: 'MoveToRestingPosition',
messageType: 'ada_feeding_msgs/action/MoveTo'
}
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToRestingPosition] = {
actionName: 'MoveFromMouthToRestingPosition',
messageType: 'ada_feeding_msgs/action/MoveTo'
}
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToStagingConfiguration] = {
actionName: 'MoveToStagingConfiguration',
messageType: 'ada_feeding_msgs/action/MoveTo'
}
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToStagingConfiguration] = {
actionName: 'MoveFromMouthToStagingConfiguration',
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouth] = {
actionName: 'MoveFromMouth',
messageType: 'ada_feeding_msgs/action/MoveTo'
}
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToMouth] = {
Expand All @@ -119,6 +109,10 @@ ROS_SERVICE_NAMES[MEAL_STATE.R_DetectingFace] = {
export { ROS_SERVICE_NAMES }
export const CLEAR_OCTOMAP_SERVICE_NAME = 'clear_octomap'
export const CLEAR_OCTOMAP_SERVICE_TYPE = 'std_srvs/srv/Empty'
export const GET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/get_parameters'
export const GET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/GetParameters'
export const SET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/set_parameters'
export const SET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/SetParameters'

/**
* The meaning of the status that motion actions return in their results.
Expand Down
47 changes: 24 additions & 23 deletions feedingwebapp/src/Pages/Footer/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { useMediaQuery } from 'react-responsive'
import PropTypes from 'prop-types'
// Local imports
import { MOVING_STATE_ICON_DICT } from '../Constants'
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.
*
* @param {string} mealState - the current meal state
* @param {bool} paused - whether the robot is currently paused
* @param {function} pauseCallback - callback function for when the pause button
* is clicked
Expand All @@ -27,14 +27,12 @@ import { useGlobalState } from '../GlobalState'
* button is clicked. If null, don't render the resume button.
*/
const Footer = (props) => {
// Get the current meal state
const mealState = useGlobalState((state) => state.mealState)
// Flag to check if the current orientation is portrait
const isPortrait = useMediaQuery({ query: '(orientation: portrait)' })
// Icons for the footer buttons
let pauseIcon = '/robot_state_imgs/pause_button_icon.svg'
let backIcon = props.backMealState ? MOVING_STATE_ICON_DICT[props.backMealState] : ''
let resumeIcon = MOVING_STATE_ICON_DICT[mealState]
let resumeIcon = MOVING_STATE_ICON_DICT[props.mealState]
// Sizes (width, height, fontsize) of footer buttons
let pauseButtonWidth = '98vw'
let backResumeButtonWidth = '47vw'
Expand Down Expand Up @@ -95,7 +93,7 @@ const Footer = (props) => {
(config) => {
return (
<>
<Row className='justify-content-center'>
<Row className='justify-content-center' style={{ width: '100%' }}>
<Button
variant={config.variant}
disabled={config.disabled}
Expand Down Expand Up @@ -163,34 +161,37 @@ const Footer = (props) => {

// Render the component
return (
<View>
<View style={{ wdith: '100%' }}>
<MDBFooter bgColor='dark' className='text-center text-lg-left' style={{ width: '100vw' }}>
<div className='text-center' style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)', paddingBottom: '5px', paddingTop: '5px' }}>
{props.paused ? (
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%' }}>
<View
style={{ flex: 5, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
>
{props.backCallback ? renderFooterButton(buttonConfig.back) : <></>}
</View>
<View
style={{ flex: 5, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
>
{props.resumeCallback ? renderFooterButton(buttonConfig.resume) : <></>}
</View>
</View>
) : (
renderFooterButton(buttonConfig.pause)
)}
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%' }}>
{props.paused ? (
<>
<View
style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
>
{props.backCallback ? renderFooterButton(buttonConfig.back) : <></>}
</View>
<View
style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
>
{props.resumeCallback ? renderFooterButton(buttonConfig.resume) : <></>}
</View>
</>
) : (
renderFooterButton(buttonConfig.pause)
)}
</View>
</div>
</MDBFooter>
</View>
)
}
Footer.propTypes = {
mealState: PropTypes.string.isRequired,
paused: PropTypes.bool.isRequired,
pauseCallback: PropTypes.func.isRequired,
// If any of the below three are null, the Footer won't render that button
// If any of the below two are null, the Footer won't render that button
resumeCallback: PropTypes.func,
backCallback: PropTypes.func,
backMealState: PropTypes.string
Expand Down
108 changes: 69 additions & 39 deletions feedingwebapp/src/Pages/GlobalState.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,9 @@ export const APP_PAGE = {
* - R_DetectingFace: Waiting for the robot to detect a face.
* - R_MovingToMouth: Waiting for the robot to finish moving to the user's
* mouth.
* - R_MovingFromMouthToStagingConfiguration: Waiting for the robot to move
* - R_MovingFromMouth: Waiting for the robot to move
* from the user's mouth to the staging configuration. This is a separate
* action from R_MovingToStagingConfiguration to allow us to customize the
* departure from the mouth (e.g., a slower speed).
* - R_MovingFromMouthToAbovePlate: Waiting for the robot to move from the
* user's mouth to above the plate. This is a separate action from
* R_MovingAbovePlate to allow us to customize the departure from the mouth
* (e.g., a slower speed).
* - R_MovingFromMouthToRestingPosition: Waiting for the robot to move from
* the user's mouth to resting position. This is a separate action from
* R_MovingToRestingPosition to allow us to customize the departure from
* the mouth (e.g., a slower speed).
* action from R_MovingToStagingConfiguration since it is cartesian.
* - U_BiteDone: Waiting for the user to indicate that they are done eating
* the bite.
* - R_StowingArm: Waiting for the robot to stow the arm.
Expand All @@ -67,14 +58,23 @@ export const MEAL_STATE = {
R_MovingToStagingConfiguration: 'R_MovingToStagingConfiguration',
R_DetectingFace: 'R_DetectingFace',
R_MovingToMouth: 'R_MovingToMouth',
R_MovingFromMouthToStagingConfiguration: 'R_MovingFromMouthToStagingConfiguration',
R_MovingFromMouthToAbovePlate: 'R_MovingFromMouthToAbovePlate',
R_MovingFromMouthToRestingPosition: 'R_MovingFromMouthToRestingPosition',
R_MovingFromMouth: 'R_MovingFromMouth',
U_BiteDone: 'U_BiteDone',
R_StowingArm: 'R_StowingArm',
U_PostMeal: 'U_PostMeal'
}

/**
* SETTINGS_STATE controls which settings page to display.
* - MAIN: The main page, with options to navigate to the other pages.
* - BITE_TRANSFER: The bite transfer page, where the user can configure
* parameters for bite transfer.
*/
export const SETTINGS_STATE = {
MAIN: 'MAIN',
BITE_TRANSFER: 'BITE_TRANSFER'
}

/**
* The parameters that users can set (keys) and a list of human-readable values
* they can take on.
Expand All @@ -90,11 +90,11 @@ export const MEAL_STATE = {
* TODO (amaln): When we connect this to ROS, each of these settings types and
* value options will have to have corresponding rosparam names and value options.
*/
export const SETTINGS = {
stagingPosition: ['In Front of Me', 'On My Right Side'],
biteInitiation: ['Open Mouth', 'Say "I am Ready"', 'Press Button'],
biteSelection: ['Name of Food', 'Click on Food']
}
// export const SETTINGS = {
// stagingPosition: ['In Front of Me', 'On My Right Side'],
// biteInitiation: ['Open Mouth', 'Say "I am Ready"', 'Press Button'],
// biteSelection: ['Name of Food', 'Click on Food']
// }

/**
* useGlobalState is a hook to store and manipulate web app state that we want
Expand All @@ -104,12 +104,14 @@ export const SETTINGS = {
export const useGlobalState = create(
persist(
(set) => ({
// The current app page
appPage: APP_PAGE.Home,
// The app's current meal state
mealState: MEAL_STATE.U_PreMeal,
// The timestamp when the robot transitioned to its current meal state
mealStateTransitionTime: Date.now(),
// The current app page
appPage: APP_PAGE.Home,
// The currently displayed settings page
settingsState: SETTINGS_STATE.MAIN,
// The goal for the bite acquisition action, including the most recent
// food item that the user selected in "bite selection"
biteAcquisitionActionGoal: null,
Expand All @@ -123,20 +125,44 @@ export const useGlobalState = create(
teleopIsMoving: false,
// Flag to indicate whether to auto-continue after face detection
faceDetectionAutoContinue: false,
// Whether the settings bite transfer page is currently at the user's face
// or not. This is in the off-chance that the mealState is not at the user's
// face, the settings page is, and the user refreshes -- the page should
// call MoveFromMouthToStaging instead of just MoveToStaging.
biteTransferPageAtFace: false,
// The button the user most recently clicked on the BiteDone page. In practice,
// this is the state we transition to after R_MovingFromMouth. In practice,
// it is either R_MovingAbovePlate, R_MovingToRestingPosition, or R_DetectingFace.
mostRecentBiteDoneResponse: MEAL_STATE.R_DetectingFace,
// Settings values
stagingPosition: SETTINGS.stagingPosition[0],
biteInitiation: SETTINGS.biteInitiation[0],
biteSelection: SETTINGS.biteSelection[0],
// stagingPosition: SETTINGS.stagingPosition[0],
// biteInitiation: SETTINGS.biteInitiation[0],
// biteSelection: SETTINGS.biteSelection[0],

// Setters for global state
setMealState: (mealState) =>
setAppPage: (appPage) =>
set(() => ({
mealState: mealState,
mealStateTransitionTime: Date.now()
appPage: appPage,
settingsState: SETTINGS_STATE.MAIN,
// Sometimes the settings menu leaves the robot in a paused state.
// Thus, we reset it to an unpaused state.
paused: false
})),
setAppPage: (appPage) =>
setMealState: (mealState, mostRecentBiteDoneResponse = null) =>
set(() => {
let retval = {
mealState: mealState,
mealStateTransitionTime: Date.now(),
biteTransferPageAtFace: false // Reset this flag when the meal state changes
}
if (mostRecentBiteDoneResponse) {
retval.mostRecentBiteDoneResponse = mostRecentBiteDoneResponse
}
return retval
}),
setSettingsState: (settingsState) =>
set(() => ({
appPage: appPage
settingsState: settingsState
})),
setBiteAcquisitionActionGoal: (biteAcquisitionActionGoal) =>
set(() => ({
Expand All @@ -158,18 +184,22 @@ export const useGlobalState = create(
set(() => ({
faceDetectionAutoContinue: faceDetectionAutoContinue
})),
setStagingPosition: (stagingPosition) =>
set(() => ({
stagingPosition: stagingPosition
})),
setBiteInitiation: (biteInitiation) =>
set(() => ({
biteInitiation: biteInitiation
})),
setBiteSelection: (biteSelection) =>
setBiteTransferPageAtFace: (biteTransferPageAtFace) =>
set(() => ({
biteSelection: biteSelection
biteTransferPageAtFace: biteTransferPageAtFace
}))
// setStagingPosition: (stagingPosition) =>
// set(() => ({
// stagingPosition: stagingPosition
// })),
// setBiteInitiation: (biteInitiation) =>
// set(() => ({
// biteInitiation: biteInitiation
// })),
// setBiteSelection: (biteSelection) =>
// set(() => ({
// biteSelection: biteSelection
// }))
}),
{ name: 'ada_web_app_global_state' }
)
Expand Down
21 changes: 10 additions & 11 deletions feedingwebapp/src/Pages/Header/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Navbar from 'react-bootstrap/Navbar'
import Nav from 'react-bootstrap/Nav'
import { useMediaQuery } from 'react-responsive'
// Toast generates a temporary pop-up with a timeout.
import { ToastContainer /* , toast */ } from 'react-toastify'
import { ToastContainer, toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
// ROS imports
import { useROS } from '../../ros/ros_helpers'
Expand Down Expand Up @@ -63,13 +63,13 @@ const Header = (props) => {
* started, take the user to the settings menu. Else, ask them to complete
* or terminate the meal because modifying settings.
*/
// const settingsClicked = useCallback(() => {
// if (mealState === MEAL_STATE.U_PreMeal || mealState === MEAL_STATE.U_PostMeal) {
// setAppPage(APP_PAGE.Settings)
// } else {
// toast('Please complete or terminate the feeding process to access Settings.')
// }
// }, [mealState, setAppPage])
const settingsClicked = useCallback(() => {
if (NON_MOVING_STATES.has(mealState)) {
setAppPage(APP_PAGE.Settings)
} else {
toast('Wait for robot motion to complete before accessing Settings.')
}
}, [mealState, setAppPage])

// Render the component. The NavBar will stay fixed even as we vertically scroll.
return (
Expand Down Expand Up @@ -107,14 +107,13 @@ const Header = (props) => {
>
Home
</Nav.Link>
{/* TODO: Reinstate the settings menu when we implement settings! */}
{/* <Nav.Link
<Nav.Link
onClick={settingsClicked}
className='text-dark bg-info rounded mx-1 btn-lg btn-huge p-2'
style={{ fontSize: textFontSize }}
>
Settings
</Nav.Link> */}
</Nav.Link>
</Nav>
{NON_MOVING_STATES.has(mealState) || paused || (mealState === MEAL_STATE.U_PlateLocator && teleopIsMoving === false) ? (
<Nav>
Expand Down
Loading

0 comments on commit 3006f62

Please sign in to comment.