Skip to content

Commit 1e50cfd

Browse files
committed
Merge branch 'main' into amaln/table_detector
2 parents 0227c1a + 648fa52 commit 1e50cfd

File tree

8 files changed

+389
-125
lines changed

8 files changed

+389
-125
lines changed

feeding_web_app_ros2_test/feeding_web_app_ros2_test/DummyRealSense.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22

33
# Standard imports
4+
import array
45
import threading
56

67
# Third-party imports
@@ -13,6 +14,16 @@
1314
from sensor_msgs.msg import CompressedImage, Image, CameraInfo
1415

1516

17+
# The fixed header that ROS2 Humble's compressed depth image transport plugin prepends to
18+
# the data. The exact value was empirically determined, but the below link shows the code
19+
# that prepends additional data:
20+
#
21+
# https://github.com/ros-perception/image_transport_plugins/blob/5ef79d74c4347e6a2d151df63230d5fea1357137/compressed_depth_image_transport/src/codec.cpp#L337
22+
COMPRESSED_DEPTH_16UC1_HEADER = array.array(
23+
"B", [0, 0, 0, 0, 46, 32, 133, 4, 192, 24, 60, 78]
24+
)
25+
26+
1627
class DummyRealSense(Node):
1728
"""
1829
Reads in a video file and publishes the frames as ROS2 messages.
@@ -80,7 +91,7 @@ def __init__(self):
8091
CompressedImage, "~/compressed_image", 1
8192
)
8293
self.aligned_depth_publisher = self.create_publisher(
83-
Image, "~/aligned_depth", 1
94+
CompressedImage, "~/aligned_depth", 1
8495
)
8596
self.camera_info_publisher = self.create_publisher(
8697
CameraInfo, "~/camera_info", 1
@@ -163,11 +174,24 @@ def publish_frames(self):
163174
)
164175

165176
# Configure the depth Image message
166-
depth_frame_msg = self.bridge.cv2_to_imgmsg(self.depth_frame, "passthrough")
177+
depth_frame_msg = CompressedImage()
167178
depth_frame_msg.header.frame_id = "camera_color_optical_frame"
168179
depth_frame_msg.header.stamp = (
169180
self.get_clock().now() - rclpy.duration.Duration(seconds=0.15)
170181
).to_msg()
182+
depth_frame_msg.format = "16UC1; compressedDepth"
183+
success, data = cv2.imencode(
184+
".png",
185+
self.depth_frame,
186+
# PNG compression 1 is the best speed setting, and is the setting
187+
# we use for our RealSense.
188+
[cv2.IMWRITE_PNG_COMPRESSION, 1],
189+
)
190+
if not success:
191+
raise RuntimeError("Failed to compress image")
192+
depth_frame_msg.data = (
193+
COMPRESSED_DEPTH_16UC1_HEADER.tobytes() + data.tobytes()
194+
)
171195

172196
# Configure the Camera Info
173197
self.camera_info_msg.header.stamp = depth_frame_msg.header.stamp

feeding_web_app_ros2_test/feeding_web_app_ros2_test/FaceDetection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
class FaceDetectionNode(Node):
1515
def __init__(
1616
self,
17-
face_detection_interval=90,
18-
num_images_with_face=60,
19-
open_mouth_interval=90,
20-
num_images_with_open_mouth=30,
17+
face_detection_interval=45,
18+
num_images_with_face=30,
19+
open_mouth_interval=45,
20+
num_images_with_open_mouth=15,
2121
):
2222
"""
2323
Initializes the FaceDetection node, which exposes a SetBool

feeding_web_app_ros2_test/launch/feeding_web_app_dummy_nodes_launch.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<node pkg="feeding_web_app_ros2_test" exec="DummyRealSense" name="DummyRealSense">
2424
<remap from="~/image" to="/camera/color/image_raw"/>
2525
<remap from="~/compressed_image" to="/camera/color/image_raw/compressed"/>
26-
<remap from="~/aligned_depth" to="/camera/aligned_depth_to_color/image_raw"/>
26+
<remap from="~/aligned_depth" to="/camera/aligned_depth_to_color/image_raw/compressedDepth"/>
2727
<remap from="~/camera_info" to="/camera/color/camera_info"/>
2828
<remap from="~/aligned_depth/camera_info" to="/camera/aligned_depth_to_color/camera_info"/>
2929
<param name="fps" value="15"/>

feedingwebapp/src/Pages/Constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ export const GET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/GetParameters'
130130
export const SET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/set_parameters'
131131
export const SET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/SetParameters'
132132

133+
// The names of parameters users can change in the settings menu
134+
export const DISTANCE_TO_MOUTH_PARAM = 'MoveToMouth.tree_kwargs.plan_distance_from_mouth'
135+
133136
/**
134137
* The meaning of the status that motion actions return in their results.
135138
* These should match the action definition(s).

feedingwebapp/src/Pages/GlobalState.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export const SETTINGS_STATE = {
7575
BITE_TRANSFER: 'BITE_TRANSFER'
7676
}
7777

78+
// The name of the default parameter namespace
79+
export const DEFAULT_NAMESPACE = 'default'
80+
7881
/**
7982
* useGlobalState is a hook to store and manipulate web app state that we want
8083
* to persist across re-renders and refreshes. It won't persist if cookies are
@@ -93,6 +96,7 @@ export const useGlobalState = create(
9396
mealStateTransitionTime: Date.now(),
9497
// The currently displayed settings page
9598
settingsState: SETTINGS_STATE.MAIN,
99+
settingsPresets: { current: DEFAULT_NAMESPACE, customNames: [] },
96100
// The goal for the bite acquisition action, including the most recent
97101
// food item that the user selected in "bite selection"
98102
biteAcquisitionActionGoal: null,
@@ -156,6 +160,10 @@ export const useGlobalState = create(
156160
set(() => ({
157161
settingsState: settingsState
158162
})),
163+
setSettingsPresets: (settingsPresets) =>
164+
set(() => ({
165+
settingsPresets: settingsPresets
166+
})),
159167
setBiteAcquisitionActionGoal: (biteAcquisitionActionGoal) =>
160168
set(() => ({
161169
biteAcquisitionActionGoal: biteAcquisitionActionGoal

feedingwebapp/src/Pages/Home/MealStates/BiteAcquisitionCheck.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,24 +137,24 @@ const BiteAcquisitionCheck = () => {
137137
(message) => {
138138
console.log('Got food-on-fork detection message', message)
139139
// Only auto-continue if the previous state was Bite Acquisition
140-
if (biteAcquisitionCheckAutoContinue && prevMealState === MEAL_STATE.R_BiteAcquisition && message.status === 1) {
140+
if (biteAcquisitionCheckAutoContinue && prevMealState === MEAL_STATE.R_BiteAcquisition) {
141141
let callbackFn = null
142-
if (message.probability < biteAcquisitionCheckAutoContinueProbThreshLower) {
142+
if (message.status === -1 || (message.status === 1 && message.probability < biteAcquisitionCheckAutoContinueProbThreshLower)) {
143143
console.log('No FoF. Auto-continuing in ', remainingSeconds, ' seconds')
144144
if (timerWasForFof.current === true) {
145145
clearTimer()
146146
}
147147
timerWasForFof.current = false
148148
callbackFn = acquisitionFailure
149-
} else if (message.probability > biteAcquisitionCheckAutoContinueProbThreshUpper) {
149+
} else if (message.status === 1 && message.probability > biteAcquisitionCheckAutoContinueProbThreshUpper) {
150150
console.log('FoF. Auto-continuing in ', remainingSeconds, ' seconds')
151151
if (timerWasForFof.current === false) {
152152
clearTimer()
153153
}
154154
timerWasForFof.current = true
155155
callbackFn = acquisitionSuccess
156156
} else {
157-
console.log('Not auto-continuing due to probability between thresholds')
157+
console.log('Not auto-continuing due to probability between thresholds or errors')
158158
clearTimer()
159159
}
160160
// Don't override an existing timer

feedingwebapp/src/Pages/Settings/BiteTransfer.jsx

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
33
import PropTypes from 'prop-types'
44
import { useId, Label, SpinButton } from '@fluentui/react-components'
55
import Button from 'react-bootstrap/Button'
6+
import Dropdown from 'react-bootstrap/Dropdown'
7+
import SplitButton from 'react-bootstrap/SplitButton'
68
// The Modal is a screen that appears on top of the main app, and can be toggled
79
// on and off.
810
import Modal from 'react-bootstrap/Modal'
@@ -14,9 +16,10 @@ import {
1416
GET_PARAMETERS_SERVICE_NAME,
1517
GET_PARAMETERS_SERVICE_TYPE,
1618
SET_PARAMETERS_SERVICE_NAME,
17-
SET_PARAMETERS_SERVICE_TYPE
19+
SET_PARAMETERS_SERVICE_TYPE,
20+
DISTANCE_TO_MOUTH_PARAM
1821
} from '../Constants'
19-
import { useGlobalState, MEAL_STATE, SETTINGS_STATE } from '../GlobalState'
22+
import { useGlobalState, MEAL_STATE, SETTINGS_STATE, DEFAULT_NAMESPACE } from '../GlobalState'
2023
import RobotMotion from '../Home/MealStates/RobotMotion'
2124
import DetectingFaceSubcomponent from '../Home/MealStates/DetectingFaceSubcomponent'
2225

@@ -26,6 +29,7 @@ import DetectingFaceSubcomponent from '../Home/MealStates/DetectingFaceSubcompon
2629
*/
2730
const BiteTransfer = (props) => {
2831
// Get relevant global state variables
32+
const settingsPresets = useGlobalState((state) => state.settingsPresets)
2933
const setSettingsState = useGlobalState((state) => state.setSettingsState)
3034
const globalMealState = useGlobalState((state) => state.mealState)
3135
const setPaused = useGlobalState((state) => state.setPaused)
@@ -130,27 +134,27 @@ const BiteTransfer = (props) => {
130134
let service = getParametersService.current
131135
// First, attempt to get the current distance to mouth
132136
let currentRequest = createROSServiceRequest({
133-
names: ['current.MoveToMouth.tree_kwargs.plan_distance_from_mouth']
137+
names: [settingsPresets.current.concat('.', DISTANCE_TO_MOUTH_PARAM)]
134138
})
135139
service.callService(currentRequest, (response) => {
136140
console.log('Got current plan_distance_from_mouth response', response)
137-
if (response.values[0].type === 0) {
141+
if (response.values.length === 0 || response.values[0].type === 0) {
138142
// Parameter not set
139143
// Second, attempt to get the default distance to mouth
140144
let defaultRequest = createROSServiceRequest({
141-
names: ['default.MoveToMouth.tree_kwargs.plan_distance_from_mouth']
145+
names: [DEFAULT_NAMESPACE.concat('.', DISTANCE_TO_MOUTH_PARAM)]
142146
})
143147
service.callService(defaultRequest, (response) => {
144148
console.log('Got default plan_distance_from_mouth response', response)
145-
if (response.values.length > 0) {
149+
if (response.values.length > 0 && response.values[0].type === 8) {
146150
setCurrentDistanceToMouth(getParameterValue(response.values[0]))
147151
}
148152
})
149153
} else {
150154
setCurrentDistanceToMouth(getParameterValue(response.values[0]))
151155
}
152156
})
153-
}, [getParametersService, setCurrentDistanceToMouth, setDoneButtonIsClicked, setPaused])
157+
}, [getParametersService, setCurrentDistanceToMouth, setDoneButtonIsClicked, setPaused, settingsPresets])
154158

155159
// Callback to set the distance to mouth parameter
156160
const setDistanceToMouth = useCallback(
@@ -159,7 +163,7 @@ const BiteTransfer = (props) => {
159163
let request = createROSServiceRequest({
160164
parameters: [
161165
{
162-
name: 'current.MoveToMouth.tree_kwargs.plan_distance_from_mouth',
166+
name: settingsPresets.current.concat('.', DISTANCE_TO_MOUTH_PARAM),
163167
value: {
164168
type: 8, // double array
165169
double_array_value: fullDistanceToMouth
@@ -174,23 +178,29 @@ const BiteTransfer = (props) => {
174178
}
175179
})
176180
},
177-
[setParametersService, setCurrentDistanceToMouth]
181+
[setParametersService, setCurrentDistanceToMouth, settingsPresets]
178182
)
179183

180184
// Callback to restore the distance to mouth to the default
181-
const restoreToDefaultButtonClicked = useCallback(() => {
182-
let service = getParametersService.current
183-
// Attempt to get the default distance to mouth
184-
let defaultRequest = createROSServiceRequest({
185-
names: ['default.MoveToMouth.tree_kwargs.plan_distance_from_mouth']
186-
})
187-
service.callService(defaultRequest, (response) => {
188-
console.log('Got default plan_distance_from_mouth response', response)
189-
if (response.values.length > 0) {
190-
setDistanceToMouth(getParameterValue(response.values[0]))
191-
}
192-
})
193-
}, [getParametersService, setDistanceToMouth])
185+
const restoreToPreset = useCallback(
186+
(preset) => {
187+
console.log('restoreToPreset called with', preset)
188+
let service = getParametersService.current
189+
// Attempt to get the default distance to mouth
190+
let defaultRequest = createROSServiceRequest({
191+
names: [preset.concat('.', DISTANCE_TO_MOUTH_PARAM)]
192+
})
193+
service.callService(defaultRequest, (response) => {
194+
console.log('Got plan_distance_from_mouth response', response)
195+
if (response.values.length > 0 && response.values[0].type === 8) {
196+
setDistanceToMouth(getParameterValue(response.values[0]))
197+
} else {
198+
restoreToPreset(DEFAULT_NAMESPACE)
199+
}
200+
})
201+
},
202+
[getParametersService, setDistanceToMouth]
203+
)
194204

195205
// Callback to move the robot to the mouth
196206
const moveToMouthButtonClicked = useCallback(() => {
@@ -275,7 +285,8 @@ const BiteTransfer = (props) => {
275285
flexDirection: 'column',
276286
justifyContent: 'center',
277287
alignItems: 'center',
278-
width: '100%'
288+
width: '100%',
289+
zIndex: 1
279290
}}
280291
>
281292
<Label
@@ -306,7 +317,7 @@ const BiteTransfer = (props) => {
306317
size: 'large'
307318
}}
308319
/>
309-
<Button
320+
<SplitButton
310321
variant='warning'
311322
className='mx-2 mb-2 btn-huge'
312323
size='lg'
@@ -315,10 +326,17 @@ const BiteTransfer = (props) => {
315326
width: '60%',
316327
color: 'black'
317328
}}
318-
onClick={restoreToDefaultButtonClicked}
329+
title={'Set to '.concat(DEFAULT_NAMESPACE)}
330+
onClick={() => restoreToPreset(DEFAULT_NAMESPACE)}
319331
>
320-
Set to Default
321-
</Button>
332+
{settingsPresets.customNames
333+
.filter((x) => x !== settingsPresets.current)
334+
.map((preset) => (
335+
<Dropdown.Item key={preset} onClick={() => restoreToPreset(preset)}>
336+
Set to {preset}
337+
</Dropdown.Item>
338+
))}
339+
</SplitButton>
322340
</View>
323341
<View
324342
style={{
@@ -396,7 +414,8 @@ const BiteTransfer = (props) => {
396414
distanceToMouthId,
397415
moveToMouthButtonClicked,
398416
moveAwayFromMouthButtonClicked,
399-
restoreToDefaultButtonClicked
417+
restoreToPreset,
418+
settingsPresets
400419
])
401420

402421
// When a face is detected, switch to MoveToMouth
@@ -435,18 +454,52 @@ const BiteTransfer = (props) => {
435454
<>
436455
<View
437456
style={{
438-
flex: 2,
457+
flex: 4,
439458
flexDirection: 'column',
440459
justifyContent: 'center',
441460
alignItems: 'center',
442461
width: '100%'
443462
}}
444463
>
445-
<h5 style={{ textAlign: 'center', fontSize: textFontSize }}>Customize Bite Transfer</h5>
464+
<p style={{ textAlign: 'center', fontSize: textFontSize, margin: 0 }} className='txt-huge'>
465+
Bite Transfer Settings
466+
</p>
467+
</View>
468+
<View
469+
style={{
470+
flex: 3,
471+
flexDirection: 'row',
472+
justifyContent: 'center',
473+
alignItems: 'center',
474+
width: '100%'
475+
}}
476+
>
477+
<View
478+
style={{
479+
flex: 1,
480+
justifyContent: 'center',
481+
alignItems: 'end',
482+
width: '100%'
483+
}}
484+
>
485+
<p style={{ fontSize: textFontSize, textAlign: 'right', margin: '0rem' }}>Preset:</p>
486+
</View>
487+
<View
488+
style={{
489+
flex: 1,
490+
justifyContent: 'center',
491+
alignItems: 'start',
492+
width: '100%'
493+
}}
494+
>
495+
<Button variant='secondary' disabled size='lg' style={{ marginLeft: '1rem' }}>
496+
{settingsPresets.current}
497+
</Button>
498+
</View>
446499
</View>
447500
<View
448501
style={{
449-
flex: 16,
502+
flex: 32,
450503
flexDirection: 'column',
451504
justifyContent: 'center',
452505
alignItems: 'center',
@@ -457,7 +510,7 @@ const BiteTransfer = (props) => {
457510
</View>
458511
<View
459512
style={{
460-
flex: 2,
513+
flex: 4,
461514
flexDirection: 'column',
462515
justifyContent: 'center',
463516
alignItems: 'center',

0 commit comments

Comments
 (0)