Skip to content

Commit 3006f62

Browse files
authored
Allow users to customize the distance to mouth during bite transfer (#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
1 parent 50b8c6a commit 3006f62

18 files changed

+2975
-486
lines changed

feedingwebapp/package-lock.json

Lines changed: 1828 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

feedingwebapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"@fluentui/react-components": "^9.44.1",
67
"@mapbox/node-pre-gyp": "^1.0.11",
78
"@testing-library/jest-dom": "^5.16.4",
89
"@testing-library/react": "^13.2.0",

feedingwebapp/src/Pages/Constants.js

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,9 @@ export const TIME_TO_RESET_MS = 3600000 // 1 hour in milliseconds
2323
*/
2424
let MOVING_STATE_ICON_DICT = {}
2525
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingAbovePlate] = '/robot_state_imgs/move_above_plate_position.svg'
26-
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToAbovePlate] = '/robot_state_imgs/move_above_plate_position.svg'
2726
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToRestingPosition] = '/robot_state_imgs/move_to_resting_position.svg'
28-
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToRestingPosition] = '/robot_state_imgs/move_to_resting_position.svg'
2927
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToStagingConfiguration] = '/robot_state_imgs/move_to_staging_configuration.svg'
30-
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToStagingConfiguration] = '/robot_state_imgs/move_to_staging_configuration.svg'
28+
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouth] = '/robot_state_imgs/move_to_staging_configuration.svg'
3129
MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToMouth] = '/robot_state_imgs/move_to_mouth_position.svg'
3230
MOVING_STATE_ICON_DICT[MEAL_STATE.R_StowingArm] = '/robot_state_imgs/stowing_arm_position.svg'
3331
export { MOVING_STATE_ICON_DICT }
@@ -69,10 +67,6 @@ ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingAbovePlate] = {
6967
actionName: 'MoveAbovePlate',
7068
messageType: 'ada_feeding_msgs/action/MoveTo'
7169
}
72-
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToAbovePlate] = {
73-
actionName: 'MoveFromMouthToAbovePlate',
74-
messageType: 'ada_feeding_msgs/action/MoveTo'
75-
}
7670
ROS_ACTIONS_NAMES[MEAL_STATE.U_BiteSelection] = {
7771
actionName: 'SegmentFromPoint',
7872
messageType: 'ada_feeding_msgs/action/SegmentFromPoint'
@@ -85,16 +79,12 @@ ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToRestingPosition] = {
8579
actionName: 'MoveToRestingPosition',
8680
messageType: 'ada_feeding_msgs/action/MoveTo'
8781
}
88-
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToRestingPosition] = {
89-
actionName: 'MoveFromMouthToRestingPosition',
90-
messageType: 'ada_feeding_msgs/action/MoveTo'
91-
}
9282
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToStagingConfiguration] = {
9383
actionName: 'MoveToStagingConfiguration',
9484
messageType: 'ada_feeding_msgs/action/MoveTo'
9585
}
96-
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToStagingConfiguration] = {
97-
actionName: 'MoveFromMouthToStagingConfiguration',
86+
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouth] = {
87+
actionName: 'MoveFromMouth',
9888
messageType: 'ada_feeding_msgs/action/MoveTo'
9989
}
10090
ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToMouth] = {
@@ -119,6 +109,10 @@ ROS_SERVICE_NAMES[MEAL_STATE.R_DetectingFace] = {
119109
export { ROS_SERVICE_NAMES }
120110
export const CLEAR_OCTOMAP_SERVICE_NAME = 'clear_octomap'
121111
export const CLEAR_OCTOMAP_SERVICE_TYPE = 'std_srvs/srv/Empty'
112+
export const GET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/get_parameters'
113+
export const GET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/GetParameters'
114+
export const SET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/set_parameters'
115+
export const SET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/SetParameters'
122116

123117
/**
124118
* The meaning of the status that motion actions return in their results.

feedingwebapp/src/Pages/Footer/Footer.jsx

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import { useMediaQuery } from 'react-responsive'
99
import PropTypes from 'prop-types'
1010
// Local imports
1111
import { MOVING_STATE_ICON_DICT } from '../Constants'
12-
import { useGlobalState } from '../GlobalState'
1312

1413
/**
1514
* The Footer shows a pause button. When users click it, the app tells the robot
1615
* to immediately pause and displays a back button that allows them to return to
1716
* previous state and a resume button that allows them to resume current state.
1817
*
18+
* @param {string} mealState - the current meal state
1919
* @param {bool} paused - whether the robot is currently paused
2020
* @param {function} pauseCallback - callback function for when the pause button
2121
* is clicked
@@ -27,14 +27,12 @@ import { useGlobalState } from '../GlobalState'
2727
* button is clicked. If null, don't render the resume button.
2828
*/
2929
const Footer = (props) => {
30-
// Get the current meal state
31-
const mealState = useGlobalState((state) => state.mealState)
3230
// Flag to check if the current orientation is portrait
3331
const isPortrait = useMediaQuery({ query: '(orientation: portrait)' })
3432
// Icons for the footer buttons
3533
let pauseIcon = '/robot_state_imgs/pause_button_icon.svg'
3634
let backIcon = props.backMealState ? MOVING_STATE_ICON_DICT[props.backMealState] : ''
37-
let resumeIcon = MOVING_STATE_ICON_DICT[mealState]
35+
let resumeIcon = MOVING_STATE_ICON_DICT[props.mealState]
3836
// Sizes (width, height, fontsize) of footer buttons
3937
let pauseButtonWidth = '98vw'
4038
let backResumeButtonWidth = '47vw'
@@ -95,7 +93,7 @@ const Footer = (props) => {
9593
(config) => {
9694
return (
9795
<>
98-
<Row className='justify-content-center'>
96+
<Row className='justify-content-center' style={{ width: '100%' }}>
9997
<Button
10098
variant={config.variant}
10199
disabled={config.disabled}
@@ -163,34 +161,37 @@ const Footer = (props) => {
163161

164162
// Render the component
165163
return (
166-
<View>
164+
<View style={{ wdith: '100%' }}>
167165
<MDBFooter bgColor='dark' className='text-center text-lg-left' style={{ width: '100vw' }}>
168166
<div className='text-center' style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)', paddingBottom: '5px', paddingTop: '5px' }}>
169-
{props.paused ? (
170-
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%' }}>
171-
<View
172-
style={{ flex: 5, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
173-
>
174-
{props.backCallback ? renderFooterButton(buttonConfig.back) : <></>}
175-
</View>
176-
<View
177-
style={{ flex: 5, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
178-
>
179-
{props.resumeCallback ? renderFooterButton(buttonConfig.resume) : <></>}
180-
</View>
181-
</View>
182-
) : (
183-
renderFooterButton(buttonConfig.pause)
184-
)}
167+
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%' }}>
168+
{props.paused ? (
169+
<>
170+
<View
171+
style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
172+
>
173+
{props.backCallback ? renderFooterButton(buttonConfig.back) : <></>}
174+
</View>
175+
<View
176+
style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}
177+
>
178+
{props.resumeCallback ? renderFooterButton(buttonConfig.resume) : <></>}
179+
</View>
180+
</>
181+
) : (
182+
renderFooterButton(buttonConfig.pause)
183+
)}
184+
</View>
185185
</div>
186186
</MDBFooter>
187187
</View>
188188
)
189189
}
190190
Footer.propTypes = {
191+
mealState: PropTypes.string.isRequired,
191192
paused: PropTypes.bool.isRequired,
192193
pauseCallback: PropTypes.func.isRequired,
193-
// If any of the below three are null, the Footer won't render that button
194+
// If any of the below two are null, the Footer won't render that button
194195
resumeCallback: PropTypes.func,
195196
backCallback: PropTypes.func,
196197
backMealState: PropTypes.string

feedingwebapp/src/Pages/GlobalState.jsx

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,9 @@ export const APP_PAGE = {
3939
* - R_DetectingFace: Waiting for the robot to detect a face.
4040
* - R_MovingToMouth: Waiting for the robot to finish moving to the user's
4141
* mouth.
42-
* - R_MovingFromMouthToStagingConfiguration: Waiting for the robot to move
42+
* - R_MovingFromMouth: Waiting for the robot to move
4343
* from the user's mouth to the staging configuration. This is a separate
44-
* action from R_MovingToStagingConfiguration to allow us to customize the
45-
* departure from the mouth (e.g., a slower speed).
46-
* - R_MovingFromMouthToAbovePlate: Waiting for the robot to move from the
47-
* user's mouth to above the plate. This is a separate action from
48-
* R_MovingAbovePlate to allow us to customize the departure from the mouth
49-
* (e.g., a slower speed).
50-
* - R_MovingFromMouthToRestingPosition: Waiting for the robot to move from
51-
* the user's mouth to resting position. This is a separate action from
52-
* R_MovingToRestingPosition to allow us to customize the departure from
53-
* the mouth (e.g., a slower speed).
44+
* action from R_MovingToStagingConfiguration since it is cartesian.
5445
* - U_BiteDone: Waiting for the user to indicate that they are done eating
5546
* the bite.
5647
* - R_StowingArm: Waiting for the robot to stow the arm.
@@ -67,14 +58,23 @@ export const MEAL_STATE = {
6758
R_MovingToStagingConfiguration: 'R_MovingToStagingConfiguration',
6859
R_DetectingFace: 'R_DetectingFace',
6960
R_MovingToMouth: 'R_MovingToMouth',
70-
R_MovingFromMouthToStagingConfiguration: 'R_MovingFromMouthToStagingConfiguration',
71-
R_MovingFromMouthToAbovePlate: 'R_MovingFromMouthToAbovePlate',
72-
R_MovingFromMouthToRestingPosition: 'R_MovingFromMouthToRestingPosition',
61+
R_MovingFromMouth: 'R_MovingFromMouth',
7362
U_BiteDone: 'U_BiteDone',
7463
R_StowingArm: 'R_StowingArm',
7564
U_PostMeal: 'U_PostMeal'
7665
}
7766

67+
/**
68+
* SETTINGS_STATE controls which settings page to display.
69+
* - MAIN: The main page, with options to navigate to the other pages.
70+
* - BITE_TRANSFER: The bite transfer page, where the user can configure
71+
* parameters for bite transfer.
72+
*/
73+
export const SETTINGS_STATE = {
74+
MAIN: 'MAIN',
75+
BITE_TRANSFER: 'BITE_TRANSFER'
76+
}
77+
7878
/**
7979
* The parameters that users can set (keys) and a list of human-readable values
8080
* they can take on.
@@ -90,11 +90,11 @@ export const MEAL_STATE = {
9090
* TODO (amaln): When we connect this to ROS, each of these settings types and
9191
* value options will have to have corresponding rosparam names and value options.
9292
*/
93-
export const SETTINGS = {
94-
stagingPosition: ['In Front of Me', 'On My Right Side'],
95-
biteInitiation: ['Open Mouth', 'Say "I am Ready"', 'Press Button'],
96-
biteSelection: ['Name of Food', 'Click on Food']
97-
}
93+
// export const SETTINGS = {
94+
// stagingPosition: ['In Front of Me', 'On My Right Side'],
95+
// biteInitiation: ['Open Mouth', 'Say "I am Ready"', 'Press Button'],
96+
// biteSelection: ['Name of Food', 'Click on Food']
97+
// }
9898

9999
/**
100100
* useGlobalState is a hook to store and manipulate web app state that we want
@@ -104,12 +104,14 @@ export const SETTINGS = {
104104
export const useGlobalState = create(
105105
persist(
106106
(set) => ({
107+
// The current app page
108+
appPage: APP_PAGE.Home,
107109
// The app's current meal state
108110
mealState: MEAL_STATE.U_PreMeal,
109111
// The timestamp when the robot transitioned to its current meal state
110112
mealStateTransitionTime: Date.now(),
111-
// The current app page
112-
appPage: APP_PAGE.Home,
113+
// The currently displayed settings page
114+
settingsState: SETTINGS_STATE.MAIN,
113115
// The goal for the bite acquisition action, including the most recent
114116
// food item that the user selected in "bite selection"
115117
biteAcquisitionActionGoal: null,
@@ -123,20 +125,44 @@ export const useGlobalState = create(
123125
teleopIsMoving: false,
124126
// Flag to indicate whether to auto-continue after face detection
125127
faceDetectionAutoContinue: false,
128+
// Whether the settings bite transfer page is currently at the user's face
129+
// or not. This is in the off-chance that the mealState is not at the user's
130+
// face, the settings page is, and the user refreshes -- the page should
131+
// call MoveFromMouthToStaging instead of just MoveToStaging.
132+
biteTransferPageAtFace: false,
133+
// The button the user most recently clicked on the BiteDone page. In practice,
134+
// this is the state we transition to after R_MovingFromMouth. In practice,
135+
// it is either R_MovingAbovePlate, R_MovingToRestingPosition, or R_DetectingFace.
136+
mostRecentBiteDoneResponse: MEAL_STATE.R_DetectingFace,
126137
// Settings values
127-
stagingPosition: SETTINGS.stagingPosition[0],
128-
biteInitiation: SETTINGS.biteInitiation[0],
129-
biteSelection: SETTINGS.biteSelection[0],
138+
// stagingPosition: SETTINGS.stagingPosition[0],
139+
// biteInitiation: SETTINGS.biteInitiation[0],
140+
// biteSelection: SETTINGS.biteSelection[0],
130141

131142
// Setters for global state
132-
setMealState: (mealState) =>
143+
setAppPage: (appPage) =>
133144
set(() => ({
134-
mealState: mealState,
135-
mealStateTransitionTime: Date.now()
145+
appPage: appPage,
146+
settingsState: SETTINGS_STATE.MAIN,
147+
// Sometimes the settings menu leaves the robot in a paused state.
148+
// Thus, we reset it to an unpaused state.
149+
paused: false
136150
})),
137-
setAppPage: (appPage) =>
151+
setMealState: (mealState, mostRecentBiteDoneResponse = null) =>
152+
set(() => {
153+
let retval = {
154+
mealState: mealState,
155+
mealStateTransitionTime: Date.now(),
156+
biteTransferPageAtFace: false // Reset this flag when the meal state changes
157+
}
158+
if (mostRecentBiteDoneResponse) {
159+
retval.mostRecentBiteDoneResponse = mostRecentBiteDoneResponse
160+
}
161+
return retval
162+
}),
163+
setSettingsState: (settingsState) =>
138164
set(() => ({
139-
appPage: appPage
165+
settingsState: settingsState
140166
})),
141167
setBiteAcquisitionActionGoal: (biteAcquisitionActionGoal) =>
142168
set(() => ({
@@ -158,18 +184,22 @@ export const useGlobalState = create(
158184
set(() => ({
159185
faceDetectionAutoContinue: faceDetectionAutoContinue
160186
})),
161-
setStagingPosition: (stagingPosition) =>
162-
set(() => ({
163-
stagingPosition: stagingPosition
164-
})),
165-
setBiteInitiation: (biteInitiation) =>
166-
set(() => ({
167-
biteInitiation: biteInitiation
168-
})),
169-
setBiteSelection: (biteSelection) =>
187+
setBiteTransferPageAtFace: (biteTransferPageAtFace) =>
170188
set(() => ({
171-
biteSelection: biteSelection
189+
biteTransferPageAtFace: biteTransferPageAtFace
172190
}))
191+
// setStagingPosition: (stagingPosition) =>
192+
// set(() => ({
193+
// stagingPosition: stagingPosition
194+
// })),
195+
// setBiteInitiation: (biteInitiation) =>
196+
// set(() => ({
197+
// biteInitiation: biteInitiation
198+
// })),
199+
// setBiteSelection: (biteSelection) =>
200+
// set(() => ({
201+
// biteSelection: biteSelection
202+
// }))
173203
}),
174204
{ name: 'ada_web_app_global_state' }
175205
)

feedingwebapp/src/Pages/Header/Header.jsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Navbar from 'react-bootstrap/Navbar'
66
import Nav from 'react-bootstrap/Nav'
77
import { useMediaQuery } from 'react-responsive'
88
// Toast generates a temporary pop-up with a timeout.
9-
import { ToastContainer /* , toast */ } from 'react-toastify'
9+
import { ToastContainer, toast } from 'react-toastify'
1010
import 'react-toastify/dist/ReactToastify.css'
1111
// ROS imports
1212
import { useROS } from '../../ros/ros_helpers'
@@ -63,13 +63,13 @@ const Header = (props) => {
6363
* started, take the user to the settings menu. Else, ask them to complete
6464
* or terminate the meal because modifying settings.
6565
*/
66-
// const settingsClicked = useCallback(() => {
67-
// if (mealState === MEAL_STATE.U_PreMeal || mealState === MEAL_STATE.U_PostMeal) {
68-
// setAppPage(APP_PAGE.Settings)
69-
// } else {
70-
// toast('Please complete or terminate the feeding process to access Settings.')
71-
// }
72-
// }, [mealState, setAppPage])
66+
const settingsClicked = useCallback(() => {
67+
if (NON_MOVING_STATES.has(mealState)) {
68+
setAppPage(APP_PAGE.Settings)
69+
} else {
70+
toast('Wait for robot motion to complete before accessing Settings.')
71+
}
72+
}, [mealState, setAppPage])
7373

7474
// Render the component. The NavBar will stay fixed even as we vertically scroll.
7575
return (
@@ -107,14 +107,13 @@ const Header = (props) => {
107107
>
108108
Home
109109
</Nav.Link>
110-
{/* TODO: Reinstate the settings menu when we implement settings! */}
111-
{/* <Nav.Link
110+
<Nav.Link
112111
onClick={settingsClicked}
113112
className='text-dark bg-info rounded mx-1 btn-lg btn-huge p-2'
114113
style={{ fontSize: textFontSize }}
115114
>
116115
Settings
117-
</Nav.Link> */}
116+
</Nav.Link>
118117
</Nav>
119118
{NON_MOVING_STATES.has(mealState) || paused || (mealState === MEAL_STATE.U_PlateLocator && teleopIsMoving === false) ? (
120119
<Nav>

0 commit comments

Comments
 (0)