Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
78a7d55
fix(demos/sensor-diagnostics): show real values in check-demo.sh
bburda Sep 28, 2026
337c2ff
fix(demos/turtlebot3): show real values and fix the fault trigger
bburda Sep 28, 2026
a207b6e
fix(demos/ota-nav2-sensor-fix): name the robot the image actually builds
bburda Sep 28, 2026
8555c5e
fix(demos/moveit): report real goal status and drop null fields
bburda Sep 28, 2026
9291a08
fix(demos/multi-ecu): inject/restore scripts work on first run
bburda Sep 28, 2026
fd3c9ed
test(sensor-diagnostics): check check-demo.sh sensor sections against…
bburda Sep 28, 2026
eb6d75e
test(sensor-diagnostics): cap the direct read echoed by a failed sect…
bburda Sep 28, 2026
563e020
test(turtlebot3): strip ANSI colours without a shellcheck disable
bburda Sep 28, 2026
72d71e0
test(turtlebot3): run the hinted inject and restore the demo after it
bburda Sep 28, 2026
bd9892d
test(turtlebot3): judge localization from AMCL's spread, not the faul…
bburda Sep 28, 2026
689b7a7
fix(tests/moveit): keep the real exit status through the cleanup trap
bburda Sep 28, 2026
3ac180d
test(moveit): check helper output against real goal status and API va…
bburda Sep 28, 2026
8fcf296
test(turtlebot3): wait for Nav2 before injecting the navigation failure
bburda Sep 28, 2026
1b31245
fix(demos/turtlebot3): re-localize AMCL in restore-normal from the si…
bburda Sep 28, 2026
b8ed687
build(demos/turtlebot3): copy container scripts after the workspace b…
bburda Sep 28, 2026
414ad79
test(turtlebot3): restore only through restore-normal.sh and check AM…
bburda Sep 28, 2026
b1c545b
fix(demos/moveit): send a goal again when the controller never answer…
bburda Sep 28, 2026
98b4983
fix(demos/multi-ecu): keep path-planner parameters writable during a …
bburda Sep 28, 2026
3cd5208
fix(demos/multi-ecu): name failed parameter writes, drop restore retries
bburda Sep 28, 2026
b04d8f9
test(multi-ecu): check every script write, restore time and failure m…
bburda Sep 28, 2026
9631b69
fix(demos/turtlebot3): take one model pose per Gazebo read
bburda Sep 28, 2026
a88ead7
test(multi-ecu): move restore targets away first, guard script copies…
bburda Sep 28, 2026
afce7f1
Merge multi-ECU script fixes
bburda Sep 28, 2026
cf23d16
test(moveit): bound every ROS wait and require a real status for ever…
bburda Sep 28, 2026
2bf3e00
fix(demos/sensor-diagnostics): wait for linked sensor data in check-d…
bburda Sep 28, 2026
21ff2f7
test(sensor-diagnostics): run check-demo.sh before linking and compar…
bburda Sep 28, 2026
dc7e47e
Merge moveit helper script fixes
bburda Sep 28, 2026
1c51000
test(turtlebot3): compare check-entities.sh and check-faults.sh outpu…
bburda Sep 28, 2026
ff9afc6
test(turtlebot3): park away from spawn before the inject and require …
bburda Sep 28, 2026
4228c98
docs(tests): state why the debounce smoke test reports faults directly
bburda Sep 28, 2026
7883fa4
Merge sensor, turtlebot3 and OTA helper script fixes
bburda Sep 28, 2026
2550a2e
docs(demos/turtlebot3): make the README API examples print real values
bburda Sep 28, 2026
2296fc1
test(multi-ecu): run restore-normal first on every ECU after start
bburda Sep 28, 2026
d5a749b
Merge multi-ECU restore-first coverage
bburda Sep 28, 2026
81702ba
fix(demos/turtlebot3): fail restore-normal when the gateway refuses a…
bburda Sep 28, 2026
ae5b8e3
test(turtlebot3): check that restore-normal reports a refused write
bburda Sep 28, 2026
29441ce
fix(demos/moveit): report a failed fault read instead of "no faults"
bburda Sep 28, 2026
8414038
fix(demos): report a failed fault list read instead of no faults
bburda Sep 28, 2026
0087513
Merge moveit fault-read reporting
bburda Sep 28, 2026
2430b7d
test: check the scripts report a failed fault list read
bburda Sep 28, 2026
b9e70a0
Merge failed-read reporting, restore refusals and README examples
bburda Sep 28, 2026
8615e9f
fix(demos/multi-ecu): fail restore-normal when the fault clear fails
bburda Sep 29, 2026
bb8fe33
fix(demos/moveit): send again only goals that were sent, use the loca…
bburda Sep 29, 2026
2b2b002
fix(demos/moveit): print a hint when joint states have no data
bburda Sep 29, 2026
5be1245
test(sensor-diagnostics): pin check-demo.sh's first run, a failed IMU…
bburda Sep 29, 2026
81b69d1
fix(demos/sensor-diagnostics): go on without a silent sensor and map …
bburda Sep 29, 2026
8c2a8d7
test(turtlebot3): check-entities.sh without LiDAR data and the fault …
bburda Sep 29, 2026
4cfaebd
test(multi-ecu): record a missed pause and check the reported clear s…
bburda Sep 29, 2026
fd13ceb
docs(multi-ecu): say what holds after a failed fault clear
bburda Sep 29, 2026
3c91cc0
fix(demos/turtlebot3): print the LiDAR hint when the scan has no data
bburda Sep 29, 2026
216933e
docs(demos/turtlebot3): say which faults fire the demo's fault trigger
bburda Sep 29, 2026
605cac2
Merge restore-normal fault clear reporting
bburda Sep 29, 2026
24650de
test(moveit): prove the cold ros2 daemon and check joint values
bburda Sep 29, 2026
8718105
Merge move-arm goal handling and joint state hints
bburda Sep 29, 2026
41683d8
test(sensor-diagnostics): hold the LiDAR on a fresh gateway so the re…
bburda Sep 29, 2026
80b0a41
fix(demos/sensor-diagnostics): read DATA_WAIT_SEC as a decimal number
bburda Sep 29, 2026
b36e634
test(turtlebot3): prove the trigger watcher live before the navigatio…
bburda Sep 29, 2026
5fe2134
Merge check-demo wait, sub-path mapping and TB3 hints
bburda Sep 29, 2026
9ff2a4a
test(moveit): pin the accepted goal that is never sent twice, and the…
bburda Sep 29, 2026
9e676a1
test(sensor-diagnostics): link a sensor after the first-message windo…
bburda Sep 29, 2026
04a3b92
fix(demos/sensor-diagnostics): keep reading sensors past their first-…
bburda Sep 29, 2026
82402bd
Merge check-demo late-link wait and sensor summary
bburda Sep 29, 2026
c7d92d6
Merge accepted-goal and joint state setup checks
bburda Sep 29, 2026
afe903f
test: keep early-exiting readers off pipes whose status the smoke tes…
bburda Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions demos/moveit_pick_place/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,27 @@ Use the interactive arm controller to send joint trajectories:
```

The script sends goals directly to the `panda_arm_controller/follow_joint_trajectory` action.
It works both from outside (via `docker exec`) and from inside the container.
It works both from outside (via `docker exec`, no TTY required) and from inside the container.
Where there is no `docker` CLI (inside the container), it uses the local `ros2`. Where there
is one, it uses a local `ros2` only if `ros2 action list` shows the arm action; it asks twice,
because a first listing without a running `ros2` daemon can miss it. Otherwise it runs the
goal through `docker exec` in the demo container.

`pick_place_loop.py` keeps sending its own goals to the same controller, so a manual move
can be preempted mid-motion by the demo's own workload. `move-arm.sh` reports the goal's
real final status: if the controller aborts it (`error_string: Current goal preempted by
new incoming action`), the script prints `Failed: <pose> (status: ABORTED)` and exits
non-zero instead of claiming success. `./move-arm.sh demo` runs all three steps regardless
of earlier failures and reports each one; the command exits non-zero if any step failed.

Each `ros2 action send_goal` run is limited to 30 seconds. The controller can fail to
deliver the goal response to a freshly started CLI (the container log shows `Failed to send
goal response`); it then never runs that goal. When the CLI printed `Sending goal:` and no
goal response arrived, the script sends the goal again, up to three times. A goal that was
accepted is never sent twice: if its result does not arrive in time, the script prints
`Failed: <pose> (status: UNKNOWN)`. A goal that was never sent (no demo container, no action
server within 30 seconds, a `ros2` error) is not sent again: the script prints the command's
output and `Failed: <pose> (goal not sent: ...)`, and exits non-zero.

### 4. Viewing Logs

Expand Down Expand Up @@ -209,6 +229,10 @@ curl http://localhost:8080/api/v1/apps/move-group/operations | jq
curl http://localhost:8080/api/v1/faults | jq
```

While the fault manager is not available (for example right after startup), `GET /faults`
answers `503`. `./check-faults.sh` then prints `Could not read faults (HTTP 503)` and exits
non-zero; it reports "No active faults" only after a successful read of an empty list.

### Clear All Faults

```bash
Expand Down Expand Up @@ -308,8 +332,8 @@ The gateway supports condition-based triggers that fire when specific events occ

### How It Works

1. `setup-triggers.sh` creates a trigger via `POST /api/v1/apps/manipulation_monitor/triggers`:
- **Resource:** `/api/v1/apps/manipulation_monitor/faults` (watches fault collection)
1. `setup-triggers.sh` creates a trigger via `POST /api/v1/apps/manipulation-monitor/triggers`:
- **Resource:** `/api/v1/apps/manipulation-monitor/faults` (watches fault collection)
- **Condition:** `OnChange` (fires on any new or updated fault)
- **Multishot:** `true` (fires repeatedly, not just once)
- **Lifetime:** 3600 seconds (auto-expires after 1 hour)
Expand All @@ -320,23 +344,23 @@ The gateway supports condition-based triggers that fire when specific events occ

```bash
# Create a trigger
curl -X POST http://localhost:8080/api/v1/apps/manipulation_monitor/triggers \
curl -X POST http://localhost:8080/api/v1/apps/manipulation-monitor/triggers \
-H "Content-Type: application/json" \
-d '{
"resource": "/api/v1/apps/manipulation_monitor/faults",
"resource": "/api/v1/apps/manipulation-monitor/faults",
"trigger_condition": {"condition_type": "OnChange"},
"multishot": true,
"lifetime": 3600
}' | jq

# List triggers
curl http://localhost:8080/api/v1/apps/manipulation_monitor/triggers | jq
curl http://localhost:8080/api/v1/apps/manipulation-monitor/triggers | jq

# Watch events (replace TRIGGER_ID)
curl -N http://localhost:8080/api/v1/apps/manipulation_monitor/triggers/TRIGGER_ID/events
curl -N http://localhost:8080/api/v1/apps/manipulation-monitor/triggers/TRIGGER_ID/events

# Delete a trigger
curl -X DELETE http://localhost:8080/api/v1/apps/manipulation_monitor/triggers/TRIGGER_ID
curl -X DELETE http://localhost:8080/api/v1/apps/manipulation-monitor/triggers/TRIGGER_ID
```

## Fault Injection Scenarios
Expand Down Expand Up @@ -445,7 +469,7 @@ Container scripts are stored under `/var/lib/ros2_medkit/scripts/moveit-planning
| Docker build fails | Apt package missing | Check if MoveIt 2 Jazzy packages are available |
| "MoveGroup not available" | Slow startup | Wait 60-90 seconds after container starts |
| Controller not loading | Missing config | Verify `moveit_controllers.yaml` is correct |
| Joint states empty | Controllers not loaded | Check `ros2 control list_controllers` inside container |
| Joint states empty (`check-entities.sh` prints "Joint state data not available") | Controllers not loaded | Check `ros2 control list_controllers` inside container |
| `ros2` CLI hangs in `docker exec` | DDS discovery across container boundaries | Use gateway REST API instead of `ros2` CLI for parameter/service operations |

## Comparison with Other Demos
Expand Down
25 changes: 16 additions & 9 deletions demos/moveit_pick_place/check-entities.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,31 @@ echo_step "1. Areas (Functional Groupings)"
curl -s "${API_BASE}/areas" | jq '.items[] | {id: .id, name: .name, description: .description}'

echo_step "2. Components (Hardware/Logical Units)"
curl -s "${API_BASE}/components" | jq '.items[] | {id: .id, name: .name, type: .type, area: .area}'
curl -s "${API_BASE}/components" | jq '.items[] | {id: .id, name: .name, description: .description}'

echo_step "3. Apps (ROS 2 Nodes)"
curl -s "${API_BASE}/apps" | jq '.items[] | {id: .id, name: .name, category: .category, component: .is_located_on}'
curl -s "${API_BASE}/apps" | jq '.items[] | {id: .id, name: .name, description: .description, component: .["x-medkit"].component_id}'

echo_step "4. Functions (High-level Capabilities)"
curl -s "${API_BASE}/functions" | jq '.items[] | {id: .id, name: .name, category: .category, hosted_by: .hosted_by}'
curl -s "${API_BASE}/functions" | jq '.items[] | {id: .id, name: .name, description: .description}'

echo_step "5. Sample Data (Joint States)"
echo "Getting latest joint states from Panda arm..."
curl -s "${API_BASE}/apps/joint-state-broadcaster/data/joint_states" 2>/dev/null | jq '{
joint_names: .data.name,
positions: .data.position,
velocities: .data.velocity
}' || echo " (Joint state data not available — robot may still be starting)"
# An error body or a reply without data (no /joint_states publisher) has no
# joint names, and jq still exits 0 on it.
JOINT_STATES=$(curl -s "${API_BASE}/apps/joint-state-broadcaster/data/joint_states" 2>/dev/null) || JOINT_STATES=""
if jq -e '.data.name | arrays | length > 0' <<< "${JOINT_STATES}" > /dev/null 2>&1; then
jq '{
joint_names: .data.name,
positions: .data.position,
velocities: .data.velocity
}' <<< "${JOINT_STATES}"
else
echo " (Joint state data not available - the robot may still be starting, or joint_state_broadcaster is not running)"
fi

echo_step "6. Faults"
curl -s "${API_BASE}/faults" | jq '.items[] | {code: .code, severity: .severity, reporter: .reporter_id}'
curl -s "${API_BASE}/faults" | jq '.items[] | {code: .fault_code, severity: .severity_label, status: .status, sources: .reporting_sources}'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same dead fallback in the joint-states block above: a 404 for data/joint_states prints "joint_names": null and the hint never runs, because jq exits 0 on the error body. Use jq -e '.data | select(. != null) | {...}'.


echo ""
echo -e "${GREEN}✓ Entity hierarchy exploration complete!${NC}"
Expand Down
15 changes: 11 additions & 4 deletions demos/moveit_pick_place/check-faults.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,19 @@ fi
echo "✓ Gateway is healthy"
echo ""

# Get all faults
# Get all faults. A failed read is not "no faults": the gateway answers 503
# while the fault manager is unavailable (for example during startup).
echo "📋 Active Faults:"
FAULTS=$(curl -s "${API_BASE}/faults")
RESPONSE=$(curl -s -m 30 -w '\n%{http_code}' "${API_BASE}/faults") || true
HTTP_CODE="${RESPONSE##*$'\n'}"
FAULTS="${RESPONSE%$'\n'*}"

# Check if there are any faults
FAULT_COUNT=$(echo "$FAULTS" | jq '.items | length')
if [ "$HTTP_CODE" != "200" ] || ! FAULT_COUNT=$(echo "$FAULTS" | jq -e '.items | arrays | length' 2>/dev/null); then
DETAIL=$(echo "$FAULTS" | jq -r '[.message, .parameters.details] | map(select(. != null)) | join(": ")' 2>/dev/null)
echo "❌ Could not read faults (HTTP ${HTTP_CODE:-000})${DETAIL:+: ${DETAIL}}"
echo " The fault list is unknown, not empty. Retry in a few seconds."
exit 1
fi

if [ "$FAULT_COUNT" = "0" ]; then
echo " No active faults — system is healthy! ✅"
Expand Down
167 changes: 139 additions & 28 deletions demos/moveit_pick_place/move-arm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@

set -eu

CONTAINER="${CONTAINER_NAME:-$(docker ps --format '{{.Names}}' | grep -E '^moveit_medkit_demo(_nvidia)?(_local)?$' | head -n1)}"
ACTION="/panda_arm_controller/follow_joint_trajectory"
JOINT_NAMES='["panda_joint1","panda_joint2","panda_joint3","panda_joint4","panda_joint5","panda_joint6","panda_joint7"]'

# Duration in seconds for trajectory execution
DURATION_SEC=3

# Time limit for one `ros2 action send_goal` run, and how many runs per goal.
# The controller can fail to deliver the goal response to a new CLI
# ("Failed to send goal response"). It then never runs the goal and the CLI
# waits forever, so a goal with no response is sent again.
SEND_TIMEOUT_SEC=30
SEND_ATTEMPTS=3

# --- Preset joint positions (radians) ---
# Ready: default MoveIt pose (from SRDF)
READY="[0.0, -0.785, 0.0, -2.356, 0.0, 1.571, 0.785]"
Expand All @@ -45,6 +51,51 @@ RIGHT="[1.5, -0.785, 0.0, -2.356, 0.0, 1.571, 0.785]"
WAVE="[0.0, -1.0, 0.0, -0.5, 0.0, 2.5, 0.785]"


have() {
command -v "$1" &> /dev/null
}

# True only if a LOCAL ros2 can actually reach the target action server.
# `ros2 node list` exits 0 even on an empty graph (wrong ROS_DOMAIN_ID, no
# multicast route), so a host with ROS 2 sourced but not connected to the
# demo looks identical to being inside the container. Checking that the
# action itself is listed avoids that false positive. A cold listing (no
# ros2 daemon yet) can miss a running server, so a miss is listed once more.
can_reach_action_locally() {
have ros2 || return 1
ros2 action list 2> /dev/null | grep -qFx "${ACTION}" \
|| ros2 action list 2> /dev/null | grep -qFx "${ACTION}"
}

# Picks how goals are sent: USE_LOCAL=true for the local ros2, else
# `docker exec` into CONTAINER. Without a docker CLI the script runs inside
# the container or on a ROS host, so the local ros2 is the only way.
# On failure sets NOT_SENT_REASON and returns 1.
USE_LOCAL=""
CONTAINER=""
NOT_SENT_REASON=""
choose_transport() {
[[ -z "${USE_LOCAL}" ]] || return 0
if ! have docker; then
if ! have ros2; then
NOT_SENT_REASON="needs the docker CLI or a sourced ROS 2 (ros2 CLI), found neither"
return 1
fi
USE_LOCAL=true
return 0
fi
if can_reach_action_locally; then
USE_LOCAL=true
return 0
fi
CONTAINER="${CONTAINER_NAME:-$(docker ps --format '{{.Names}}' | grep -E '^moveit_medkit_demo(_nvidia)?(_local)?$' | head -n1)}"
if [[ -z "${CONTAINER}" ]]; then
NOT_SENT_REASON="no running moveit_medkit_demo container, start it with ./run-demo.sh or set CONTAINER_NAME"
return 1
fi
USE_LOCAL=false
}

send_trajectory() {
local positions="$1"
local label="$2"
Expand All @@ -64,27 +115,76 @@ send_trajectory() {
}
}"

# Check if we're inside the container or outside
if command -v ros2 &> /dev/null && ros2 node list &> /dev/null 2>&1; then
# Inside the container (or ROS 2 env is set up)
ros2 action send_goal "${ACTION}" \
control_msgs/action/FollowJointTrajectory \
"${goal_msg}" \
--feedback
else
# Outside — exec into container
docker exec -it "${CONTAINER}" bash -c "
source /opt/ros/jazzy/setup.bash && \
source /root/demo_ws/install/setup.bash && \
ros2 action send_goal ${ACTION} \
control_msgs/action/FollowJointTrajectory \
\"${goal_msg}\" \
--feedback
"
if ! choose_transport; then
echo "Failed: ${label} (goal not sent: ${NOT_SENT_REASON})" >&2
return 1
fi

# `ros2 action send_goal` always exits 0, whatever the goal's outcome -
# the real result is in its own printed "Goal finished with status:"
# line, so capture output and parse that instead of the exit code.
# PYTHONUNBUFFERED keeps the lines printed before a timeout kills the CLI.
local attempt output rc
for ((attempt = 1; attempt <= SEND_ATTEMPTS; attempt++)); do
rc=0
if [[ "${USE_LOCAL}" == true ]]; then
output=$(PYTHONUNBUFFERED=1 timeout "${SEND_TIMEOUT_SEC}" \
ros2 action send_goal "${ACTION}" \
control_msgs/action/FollowJointTrajectory \
"${goal_msg}" \
--feedback 2>&1) || rc=$?
else
# Outside the container: exec into it. No -it: this must also
# work without a TTY (CI, a pipe), and the command needs no stdin.
# timeout runs in the container: killing `docker exec` would
# leave the CLI running there.
output=$(docker exec "${CONTAINER}" bash -c "
source /opt/ros/jazzy/setup.bash && \
source /root/demo_ws/install/setup.bash && \
PYTHONUNBUFFERED=1 timeout ${SEND_TIMEOUT_SEC} \
ros2 action send_goal ${ACTION} \
control_msgs/action/FollowJointTrajectory \
\"${goal_msg}\" \
--feedback
" 2>&1) || rc=$?
fi
printf '%s\n' "${output}"
# An accepted or rejected goal has its answer.
if grep -qE '^(Goal accepted with ID|Goal was rejected)' <<< "${output}"; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything without a "Goal accepted/rejected" line is treated as a lost goal, including docker: command not found, a wrong CONTAINER and The passed action type is invalid; those fail instantly, yet the loop prints "No goal response within 30 s, sending the goal again" twice and ends with status: UNKNOWN. Resend only when the output shows the CLI got as far as Sending goal:, and report any other output as the failure it is.

break
fi
# Never sent (no container, no action server, a CLI error): sending
# again changes nothing. rc 124 is the timeout.
if ! grep -q '^Sending goal:' <<< "${output}"; then
if ((rc == 124)) && grep -q '^Waiting for an action server' <<< "${output}"; then
echo "Failed: ${label} (goal not sent: no action server ${ACTION} within ${SEND_TIMEOUT_SEC} s)" >&2
elif ((rc == 124)); then
echo "Failed: ${label} (goal not sent: timed out after ${SEND_TIMEOUT_SEC} s)" >&2
else
echo "Failed: ${label} (goal not sent: exit status ${rc})" >&2
fi
return 1
fi
# Sent with no response: the controller never runs it, so send again.
if ((attempt < SEND_ATTEMPTS)); then
if ((rc == 124)); then
echo "No goal response within ${SEND_TIMEOUT_SEC} s, sending the goal again"
else
echo "No goal response (exit status ${rc}), sending the goal again"
fi
fi
done

local status
status=$(printf '%s\n' "${output}" | grep -F 'Goal finished with status:' | tail -n1 | sed -E 's/.*status: *//')

echo ""
echo "✅ Done: ${label}"
if [[ "${status}" == "SUCCEEDED" ]]; then
echo "✅ Done: ${label}"
return 0
fi
echo "Failed: ${label} (status: ${status:-UNKNOWN})" >&2
return 1
}

show_menu() {
Expand All @@ -110,13 +210,19 @@ show_menu() {
run_demo_cycle() {
echo "🔄 Running pick → place → home cycle..."
echo ""
send_trajectory "${PICK}" "pick"
local failed=0
send_trajectory "${PICK}" "pick" || failed=1
sleep 2
send_trajectory "${PLACE}" "place"
send_trajectory "${PLACE}" "place" || failed=1
sleep 2
send_trajectory "${READY}" "ready (home)"
send_trajectory "${READY}" "ready (home)" || failed=1
echo ""
echo "🔄 Cycle complete!"
if [[ "${failed}" -eq 0 ]]; then
echo "🔄 Cycle complete!"
else
echo "🔄 Cycle complete with failures" >&2
fi
return "${failed}"
}

handle_choice() {
Expand All @@ -138,15 +244,20 @@ handle_choice() {

# --- Main ---

# If argument provided, run directly
# If argument provided, run directly. Reflect the goal's real result in the
# exit code instead of always exiting 0.
if [[ $# -gt 0 ]]; then
handle_choice "$1"
exit 0
if handle_choice "$1"; then
exit 0
else
exit 1
fi
fi

# Interactive mode
# Interactive mode. A failed goal reports failure and the menu continues -
# it must not kill the session (set -e would, without this guard).
while true; do
show_menu
read -rp "Choose position (1-8, d, q): " choice
handle_choice "${choice}"
handle_choice "${choice}" || true
done
9 changes: 5 additions & 4 deletions demos/multi_ecu_aggregation/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ COPY src/ ${COLCON_WS}/src/multi_ecu_demo/src/
COPY config/ ${COLCON_WS}/src/multi_ecu_demo/config/
COPY launch/ ${COLCON_WS}/src/multi_ecu_demo/launch/

# Copy container scripts
COPY container_scripts/ /var/lib/ros2_medkit/scripts/
RUN find /var/lib/ros2_medkit/scripts -name "*.bash" -exec chmod +x {} \;

# Build all packages (skip test dependencies that aren't in ros-base)
WORKDIR ${COLCON_WS}
RUN bash -c "source /opt/ros/jazzy/setup.bash && \
Expand All @@ -61,6 +57,11 @@ RUN bash -c "source /opt/ros/jazzy/setup.bash && \
--skip-keys='ament_cmake_clang_format ament_cmake_clang_tidy test_msgs example_interfaces sqlite3' && \
MAKEFLAGS='-j 2' colcon build --executor sequential --symlink-install --cmake-args -DBUILD_TESTING=OFF"

# Copy container scripts. Not a build input: kept after colcon build so a
# script-only change does not invalidate the compile cache.
COPY container_scripts/ /var/lib/ros2_medkit/scripts/
RUN find /var/lib/ros2_medkit/scripts -name "*.bash" -exec chmod +x {} \;

# Setup environment
RUN echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc && \
echo "source ${COLCON_WS}/install/setup.bash" >> ~/.bashrc
Expand Down
Loading
Loading