Skip to content

Commit d545443

Browse files
authored
Implement Web App Teleop & Arm Configuration Customization (#133)
* Added a HoldButton to the app * [WIP] began teleop subcomponent * [WIP] MVP done and tested in sim * Works on lovelace * Arranged teleop within the modal * Only allow teleop in non-moving states (WIP) * Allow customization of teleop speed * Fixes from in-person testing of speed * Unified setting of non-moving states, fixed prevMealState bug, removed PlateLocator * [WIP] adding teleop to settings * [WIP] Visual layout of AbovePlate done * [WIP] Code is in-place, need to debug * [WIP] basic functionality works, need to test it * [WIP] adjust parent view * Mostly working, few lingering bugs with action cancellation, then test in-person * Sim testing done, need to do real testing * Fixes through in-person testing * Updated min and default speeds for teleop * Got teleop working directly with controllers * Adjust default joint teleop speed * Adjust video feed formatting
1 parent f616074 commit d545443

21 files changed

+2162
-735
lines changed

feedingwebapp/src/Pages/Constants.js

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,6 @@ MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToMouth] = '/robot_state_imgs/move_to_
3030
MOVING_STATE_ICON_DICT[MEAL_STATE.R_StowingArm] = '/robot_state_imgs/stowing_arm_position.svg'
3131
export { MOVING_STATE_ICON_DICT }
3232

33-
/**
34-
* A set containing the states where the robot does not move.
35-
*
36-
* NOTE: Although in R_DetectingFace the robot does not technically move,
37-
* the app might transition out of that state into a robot motion state without
38-
* user intervention, so it is not included in this set.
39-
*/
40-
let NON_MOVING_STATES = new Set()
41-
NON_MOVING_STATES.add(MEAL_STATE.U_PreMeal)
42-
NON_MOVING_STATES.add(MEAL_STATE.U_BiteSelection)
43-
NON_MOVING_STATES.add(MEAL_STATE.U_BiteAcquisitionCheck)
44-
NON_MOVING_STATES.add(MEAL_STATE.U_BiteDone)
45-
NON_MOVING_STATES.add(MEAL_STATE.U_PostMeal)
46-
export { NON_MOVING_STATES }
47-
4833
// The names of the ROS topic(s)
4934
export const CAMERA_FEED_TOPIC = '/local/camera/color/image_raw/compressed'
5035
export const FACE_DETECTION_TOPIC = '/face_detection'
@@ -53,6 +38,10 @@ export const FACE_DETECTION_IMG_TOPIC = '/face_detection_img/compressed'
5338
export const FOOD_ON_FORK_DETECTION_TOPIC = '/food_on_fork_detection'
5439
export const FOOD_ON_FORK_DETECTION_TOPIC_MSG = 'ada_feeding_msgs/FoodOnForkDetection'
5540
export const ROBOT_COMPRESSED_IMG_TOPICS = [CAMERA_FEED_TOPIC, FACE_DETECTION_IMG_TOPIC]
41+
export const SERVO_CARTESIAN_TOPIC = '/web_app/servo_node/delta_twist_cmds'
42+
export const SERVO_CARTESIAN_TOPIC_MSG = 'geometry_msgs/msg/TwistStamped'
43+
export const SERVO_JOINT_TOPIC = '/web_app/servo_node/delta_joint_cmds'
44+
export const SERVO_JOINT_TOPIC_MSG = 'control_msgs/msg/JointJog'
5645

5746
// States from which, if they fail, it is NOT okay for the user to retry the
5847
// same action.
@@ -98,6 +87,10 @@ ROS_ACTIONS_NAMES[MEAL_STATE.R_StowingArm] = {
9887
messageType: 'ada_feeding_msgs/action/MoveTo'
9988
}
10089
export { ROS_ACTIONS_NAMES }
90+
export const START_CARTESIAN_CONTROLLER_ACTION_NAME = 'ActivateCartesianController'
91+
export const START_CARTESIAN_CONTROLLER_ACTION_TYPE = 'ada_feeding_msgs/action/Trigger'
92+
export const START_JOINT_CONTROLLER_ACTION_NAME = 'ActivateJointController'
93+
export const START_JOINT_CONTROLLER_ACTION_TYPE = 'ada_feeding_msgs/action/Trigger'
10194

10295
/**
10396
* For states that call ROS services, this dictionary contains
@@ -121,13 +114,35 @@ export const CLEAR_OCTOMAP_SERVICE_NAME = 'clear_octomap'
121114
export const CLEAR_OCTOMAP_SERVICE_TYPE = 'std_srvs/srv/Empty'
122115
export const ACQUISITION_REPORT_SERVICE_NAME = 'ada_feeding_action_select/action_report'
123116
export const ACQUISITION_REPORT_SERVICE_TYPE = 'ada_feeding_msgs/srv/AcquisitionReport'
117+
export const GET_JOINT_STATE_SERVICE_NAME = 'get_joint_state'
118+
export const GET_JOINT_STATE_SERVICE_TYPE = 'ada_feeding_msgs/srv/GetJointState'
124119
export const GET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/get_parameters'
125120
export const GET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/GetParameters'
126121
export const SET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/set_parameters'
127122
export const SET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/SetParameters'
128123

129124
// The names of parameters users can change in the settings menu
130125
export const DISTANCE_TO_MOUTH_PARAM = 'MoveToMouth.tree_kwargs.plan_distance_from_mouth'
126+
export const ABOVE_PLATE_PARAM_JOINTS = 'MoveAbovePlate.tree_kwargs.joint_positions'
127+
export const STAGING_PARAM_JOINTS = 'MoveToStagingConfiguration.tree_kwargs.goal_configuration'
128+
export const STAGING_PARAM_POSITION = 'MoveFromMouth.tree_kwargs.staging_configuration_position'
129+
export const STAGING_PARAM_ORIENTATION = 'MoveFromMouth.tree_kwargs.staging_configuration_quat_xyzw'
130+
// TODO: Eventually, we should break AcquireFood into two actionss to avoid these
131+
// two different resting parameters.
132+
export const RESTING_PARAM_JOINTS_1 = 'AcquireFood.tree_kwargs.resting_joint_positions'
133+
// TODO: We may need to remove the orientation constraint from the below action.
134+
export const RESTING_PARAM_JOINTS_2 = 'MoveToRestingPosition.tree_kwargs.goal_configuration'
135+
136+
// Robot link names
137+
export const ROBOT_BASE_LINK = 'j2n6s200_link_base'
138+
export const ROBOT_JOINTS = [
139+
'j2n6s200_joint_1',
140+
'j2n6s200_joint_2',
141+
'j2n6s200_joint_3',
142+
'j2n6s200_joint_4',
143+
'j2n6s200_joint_5',
144+
'j2n6s200_joint_6'
145+
]
131146

132147
/**
133148
* The meaning of the status that motion actions return in their results.
@@ -157,3 +172,31 @@ export const ROS_ACTION_STATUS_CANCEL_GOAL = '2'
157172
export const ROS_ACTION_STATUS_SUCCEED = '3'
158173
export const ROS_ACTION_STATUS_ABORT = '4'
159174
export const ROS_ACTION_STATUS_CANCELED = '5'
175+
176+
/**
177+
* A function that provides the text associated with robot motion for each meal state.
178+
*/
179+
export function getRobotMotionText(mealState) {
180+
switch (mealState) {
181+
case MEAL_STATE.R_MovingAbovePlate:
182+
return 'Waiting to move above the plate...'
183+
case MEAL_STATE.R_BiteAcquisition:
184+
return 'Waiting to acquire the food...'
185+
case MEAL_STATE.R_MovingToRestingPosition:
186+
return 'Waiting to move to the resting position...'
187+
case MEAL_STATE.R_MovingToStagingConfiguration:
188+
return 'Waiting to move in front of you...'
189+
case MEAL_STATE.R_MovingToMouth:
190+
return 'Waiting to move to your mouth...'
191+
case MEAL_STATE.R_MovingFromMouth:
192+
return 'Waiting to move from your mouth...'
193+
case MEAL_STATE.R_StowingArm:
194+
return 'Waiting to stow the arm...'
195+
default:
196+
return 'Unknown meal state' + mealState.toString()
197+
}
198+
}
199+
200+
// Container IDs for multiple ToastContainers
201+
export const REGULAR_CONTAINER_ID = 'RegularContainerID'
202+
export const MODAL_CONTAINER_ID = 'ModalContainerID'

feedingwebapp/src/Pages/GlobalState.jsx

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ export const APP_PAGE = {
2626
* - U_PreMeal: Waiting for the user to click "Start Feeding."
2727
* - R_MovingAbovePlate: Waiting for the robot to move above the plate.
2828
* - U_BiteSelection: Waiting for the user to select the food item they want.
29-
* - U_PlateLocator: Allows the user to teleoperate the robot to center the
30-
* the plate.
3129
* - R_BiteAcquisition: Waiting for the robot to execute one bite acquisition
3230
* attempt.
3331
* - R_MovingToRestingPosition: Waiting for the robot to move to resting
@@ -51,7 +49,6 @@ export const MEAL_STATE = {
5149
U_PreMeal: 'U_PreMeal',
5250
R_MovingAbovePlate: 'R_MovingAbovePlate',
5351
U_BiteSelection: 'U_BiteSelection',
54-
U_PlateLocator: 'U_PlateLocator',
5552
R_BiteAcquisition: 'R_BiteAcquisition',
5653
R_MovingToRestingPosition: 'R_MovingToRestingPosition',
5754
U_BiteAcquisitionCheck: 'U_BiteAcquisitionCheck',
@@ -64,6 +61,18 @@ export const MEAL_STATE = {
6461
U_PostMeal: 'U_PostMeal'
6562
}
6663

64+
/**
65+
* A set containing the states where the robot does not move.
66+
*/
67+
let NON_MOVING_STATES = new Set()
68+
NON_MOVING_STATES.add(MEAL_STATE.U_PreMeal)
69+
NON_MOVING_STATES.add(MEAL_STATE.U_BiteSelection)
70+
NON_MOVING_STATES.add(MEAL_STATE.U_BiteAcquisitionCheck)
71+
NON_MOVING_STATES.add(MEAL_STATE.R_DetectingFace)
72+
NON_MOVING_STATES.add(MEAL_STATE.U_BiteDone)
73+
NON_MOVING_STATES.add(MEAL_STATE.U_PostMeal)
74+
export { NON_MOVING_STATES }
75+
6776
/**
6877
* SETTINGS_STATE controls which settings page to display.
6978
* - MAIN: The main page, with options to navigate to the other pages.
@@ -72,7 +81,8 @@ export const MEAL_STATE = {
7281
*/
7382
export const SETTINGS_STATE = {
7483
MAIN: 'MAIN',
75-
BITE_TRANSFER: 'BITE_TRANSFER'
84+
BITE_TRANSFER: 'BITE_TRANSFER',
85+
ABOVE_PLATE: 'ABOVE_PLATE'
7686
}
7787

7888
// The name of the default parameter namespace
@@ -92,6 +102,9 @@ export const useGlobalState = create(
92102
mealState: MEAL_STATE.U_PreMeal,
93103
// The app's previous meal state
94104
prevMealState: null,
105+
// Whether the app is currently in a non-moving state (i.e., the robot will
106+
// not move unless the user initiates it)
107+
inNonMovingState: true,
95108
// The timestamp when the robot transitioned to its current meal state
96109
mealStateTransitionTime: Date.now(),
97110
// The currently displayed settings page
@@ -106,10 +119,14 @@ export const useGlobalState = create(
106119
moveToMouthActionGoal: null,
107120
// Last RobotMotion action response
108121
lastMotionActionResponse: null,
109-
// Whether or not the currently-executing robot motion was paused by the user
122+
// Whether or not the currently-executing robot motion was paused by the user.
123+
// NOTE: `paused` may no longer need to be in global state now that we have
124+
// the `inNonMovingState` flag.
110125
paused: false,
111-
// Flag to indicate robot motion trough teleoperation interface
112-
teleopIsMoving: false,
126+
// Store the user;s current settings for teleop speeds
127+
teleopLinearSpeed: 0.1, // m/s
128+
teleopAngularSpeed: 0.15, // rad/s
129+
teleopJointSpeed: 0.2, // rad/s
113130
// Flag to indicate whether to auto-continue after face detection
114131
faceDetectionAutoContinue: true,
115132
// Flag to indicate whether to auto-continue in bite done after food-on-fork detection
@@ -145,17 +162,37 @@ export const useGlobalState = create(
145162
})),
146163
setMealState: (mealState, mostRecentBiteDoneResponse = null) =>
147164
set(() => {
165+
let prevMealState = get().mealState
166+
console.log('Setting meal state to', mealState, 'from', prevMealState)
148167
let retval = {
149168
mealState: mealState,
150-
prevMealState: get().mealState,
151169
mealStateTransitionTime: Date.now(),
152170
biteTransferPageAtFace: false // Reset this flag when the meal state changes
153171
}
172+
// Only update the previous state if it is not a self-transition (to
173+
// account for cases where a MoveTo action result message is reveived twice)
174+
if (prevMealState !== mealState) {
175+
retval.prevMealState = prevMealState
176+
}
154177
if (mostRecentBiteDoneResponse) {
155178
retval.mostRecentBiteDoneResponse = mostRecentBiteDoneResponse
156179
}
180+
if (NON_MOVING_STATES.has(mealState)) {
181+
retval.inNonMovingState = true
182+
console.log('Setting inNonMovingState to true through setMealState')
183+
} else {
184+
retval.inNonMovingState = false
185+
console.log('Setting inNonMovingState to false through setMealState')
186+
}
157187
return retval
158188
}),
189+
setInNonMovingState: (inNonMovingState) =>
190+
set(() => {
191+
console.log('Setting inNonMovingState to', inNonMovingState, 'through setInNonMovingState')
192+
return {
193+
inNonMovingState: inNonMovingState
194+
}
195+
}),
159196
setSettingsState: (settingsState) =>
160197
set(() => ({
161198
settingsState: settingsState
@@ -177,12 +214,34 @@ export const useGlobalState = create(
177214
moveToMouthActionGoal: moveToMouthActionGoal
178215
})),
179216
setPaused: (paused) =>
217+
set(() => {
218+
let retval = { paused: paused }
219+
if (paused) {
220+
// If the robot is paused, we should store this as a non-moving state
221+
retval.inNonMovingState = true
222+
} else {
223+
// If the robot is unpaused, we should check if the meal state is moving
224+
if (!NON_MOVING_STATES.has(get().mealState)) {
225+
retval.inNonMovingState = false
226+
console.log('Setting inNonMovingState to false through setPaused')
227+
} else {
228+
retval.inNonMovingState = true
229+
console.log('Setting inNonMovingState to true through setPaused')
230+
}
231+
}
232+
return retval
233+
}),
234+
setTeleopLinearSpeed: (teleopLinearSpeed) =>
235+
set(() => ({
236+
teleopLinearSpeed: teleopLinearSpeed
237+
})),
238+
setTeleopAngularSpeed: (teleopAngularSpeed) =>
180239
set(() => ({
181-
paused: paused
240+
teleopAngularSpeed: teleopAngularSpeed
182241
})),
183-
setTeleopIsMoving: (teleopIsMoving) =>
242+
setTeleopJointSpeed: (teleopJointSpeed) =>
184243
set(() => ({
185-
teleopIsMoving: teleopIsMoving
244+
teleopJointSpeed: teleopJointSpeed
186245
})),
187246
setFaceDetectionAutoContinue: (faceDetectionAutoContinue) =>
188247
set(() => ({

feedingwebapp/src/Pages/Header/Header.jsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import 'react-toastify/dist/ReactToastify.css'
1111
// ROS imports
1212
import { useROS } from '../../ros/ros_helpers'
1313
// Local imports
14-
import { ROS_CHECK_INTERVAL_MS, NON_MOVING_STATES } from '../Constants'
15-
import { useGlobalState, APP_PAGE, MEAL_STATE } from '../GlobalState'
16-
import LiveVideoModal from './LiveVideoModal'
14+
import { REGULAR_CONTAINER_ID, ROS_CHECK_INTERVAL_MS } from '../Constants'
15+
import { useGlobalState, APP_PAGE, NON_MOVING_STATES } from '../GlobalState'
16+
import InfoModal from './InfoModal'
1717

1818
/**
1919
* The Header component consists of the navigation bar (which has buttons Home,
@@ -23,10 +23,10 @@ import LiveVideoModal from './LiveVideoModal'
2323
*/
2424
const Header = (props) => {
2525
// Create a local state variable to toggle on/off the video
26-
// TODO: Since this local state variable is in the header, the LiveVideoModal
26+
// TODO: Since this local state variable is in the header, the InfoModal
2727
// continues showing even if the state changes. Is this desirable? Perhaps
2828
// it should close if the state changes?
29-
const [videoShow, setVideoShow] = useState(false)
29+
const [infoModalShow, setInfoModalShow] = useState(false)
3030
// useROS gives us access to functions to configure and interact with ROS.
3131
let { ros } = useROS()
3232
const [isConnected, setIsConncected] = useState(ros.isConnected)
@@ -47,9 +47,8 @@ const Header = (props) => {
4747

4848
// Get the relevant global state variables
4949
const mealState = useGlobalState((state) => state.mealState)
50+
const inNonMovingState = useGlobalState((state) => state.inNonMovingState)
5051
const setAppPage = useGlobalState((state) => state.setAppPage)
51-
const paused = useGlobalState((state) => state.paused)
52-
const teleopIsMoving = useGlobalState((state) => state.teleopIsMoving)
5352

5453
/**
5554
* When the Home button in the header is clicked, return to the Home page.
@@ -64,12 +63,18 @@ const Header = (props) => {
6463
* or terminate the meal because modifying settings.
6564
*/
6665
const settingsClicked = useCallback(() => {
67-
if (NON_MOVING_STATES.has(mealState)) {
66+
// Both checks are necessary because some states that are nominally non-moving
67+
// could be in an auto-continue state, and some states that are nominally moving
68+
// could be paused.
69+
if (inNonMovingState && NON_MOVING_STATES.has(mealState)) {
6870
setAppPage(APP_PAGE.Settings)
6971
} else {
70-
toast('Wait for robot motion to complete before accessing Settings.')
72+
toast.info('Wait for robot motion to complete before accessing Settings.', {
73+
containerId: REGULAR_CONTAINER_ID,
74+
toastId: 'noSettings'
75+
})
7176
}
72-
}, [mealState, setAppPage])
77+
}, [inNonMovingState, mealState, setAppPage])
7378

7479
// Render the component. The NavBar will stay fixed even as we vertically scroll.
7580
return (
@@ -78,7 +83,7 @@ const Header = (props) => {
7883
* The ToastContainer is an alert that pops up on the top of the screen
7984
* and has a timeout.
8085
*/}
81-
<ToastContainer style={{ fontSize: textFontSize }} />
86+
<ToastContainer style={{ fontSize: textFontSize, zIndex: 9999 }} containerId={REGULAR_CONTAINER_ID} enableMultiContainer={true} />
8287
{/**
8388
* The NavBar has two elements, Home and Settings, on the left side and three
8489
* elements, Lock, Robot Connection Icon and VideoVideo, on the right side.
@@ -115,7 +120,7 @@ const Header = (props) => {
115120
Settings
116121
</Nav.Link>
117122
</Nav>
118-
{NON_MOVING_STATES.has(mealState) || paused || (mealState === MEAL_STATE.U_PlateLocator && teleopIsMoving === false) ? (
123+
{inNonMovingState ? (
119124
<Nav>
120125
<Nav.Link
121126
className='text-dark rounded mx-1 btn-lg btn-huge p-2'
@@ -150,20 +155,20 @@ const Header = (props) => {
150155
)}
151156
<Nav>
152157
<Nav.Link
153-
onClick={() => setVideoShow(true)}
158+
onClick={() => setInfoModalShow(true)}
154159
className='text-dark bg-info rounded mx-1 btn-lg btn-huge p-2'
155160
style={{ fontSize: textFontSize }}
156161
>
157-
Video
162+
Info
158163
</Nav.Link>
159164
</Nav>
160165
</Navbar>
161166
</Navbar>
162167
{/**
163-
* The LiveVideoModal toggles on and off with the Video button and shows the
168+
* The InfoModal toggles on and off with the Video button and shows the
164169
* robot's live camera feed.
165170
*/}
166-
<LiveVideoModal show={videoShow} onHide={() => setVideoShow(false)} webrtcURL={props.webrtcURL} />
171+
<InfoModal show={infoModalShow} onHide={() => setInfoModalShow(false)} webrtcURL={props.webrtcURL} />
167172
</>
168173
)
169174
}

0 commit comments

Comments
 (0)