Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 9 additions & 6 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
ARG BASE_IMAGE=lcas.lincoln.ac.uk/lcas/limo_platform:2
ARG BASE_IMAGE=lcas.lincoln.ac.uk/lcas/limo_platform:2.2

FROM ${BASE_IMAGE} as base
FROM ${BASE_IMAGE} AS base

# making the standard global variables available for target-specific builds
ARG TARGETARCH

USER root

# get the source tree and analyse it for its package.xml only
FROM base as sourcefilter
FROM base AS sourcefilter
RUN mkdir -p /tmp/src/
COPY ./src/*/package.xml /tmp/src/_workspace/src/_pkgs_xmls
COPY ./src/*/*/package.xml /tmp/src/_workspace/src/_pkgs_xmls
Expand All @@ -19,17 +19,20 @@ COPY ./src/*/*/*/*/package.xml /tmp/src/_workspace/src/_pkgs_xmls
RUN find /tmp/src -type f \! -name "package.xml" -print | xargs rm -rf

# install all dependencies listed in the package.xml
FROM base as devcontainer
FROM base AS devcontainer
# copy the reduced source tree (only package.xml) from previous stage
COPY --from=sourcefilter /tmp/src /tmp/src
COPY .devcontainer/validate_fortress_runtime.sh /usr/local/bin/validate_fortress_runtime.sh
RUN rosdep update --rosdistro=${ROS_DISTRO} && apt-get update
RUN apt-get update && \
. /opt/ros/lcas/install/setup.sh && rosdep install --from-paths /tmp/src --ignore-src -y && \
apt-get install -y ros-${ROS_DISTRO}-rqt-reconfigure && \
chmod +x /usr/local/bin/validate_fortress_runtime.sh && \
/usr/local/bin/validate_fortress_runtime.sh && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/src


FROM devcontainer as compiled
FROM devcontainer AS compiled

COPY ./src /opt/ros/lcas/src/workspace/src
RUN . /opt/ros/lcas/install/setup.sh && \
Expand All @@ -44,4 +47,4 @@ RUN cd /opt/ros/lcas && colcon build && \

USER ros
WORKDIR /home/ros
ENV SHELL=/bin/bash
ENV SHELL=/bin/bash
5 changes: 2 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@

// Use 'postCreateCommand' to run commands after the container is created.
// Here we run `colcon build` to build the workspace initially, (to speed up later builds), and we keep going if it fails for some packages to get as much compiled as possible
"postStartCommand": "/opt/entrypoint.sh /bin/true; .devcontainer/post-create.sh",

"postStartCommand": "/opt/entrypoint.sh /bin/true; ${containerWorkspaceFolder}/.devcontainer/post-create.sh",
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
"remoteUser": "ros",
"updateRemoteUserUID": true, // ensure internal user has the same UID as the host user and update file permissions
Expand All @@ -47,4 +46,4 @@
"cpus": 2,
"memory": "6gb"
}
}
}
11 changes: 9 additions & 2 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

set -xe

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
WORKSPACE_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)


function add_config_if_not_exist {
if ! grep -F -q "$1" $HOME/.bashrc; then
Expand All @@ -19,10 +22,14 @@ add_config_if_not_exist "source /opt/ros/lcas/install/setup.bash"
source /opt/ros/humble/setup.bash
source /opt/ros/lcas/install/setup.bash

/usr/local/bin/validate_fortress_runtime.sh

cd "$WORKSPACE_DIR"

colcon build --symlink-install --continue-on-error || true

LOCAL_SETUP_FILE=`pwd`/install/setup.bash
LOCAL_SETUP_FILE="$WORKSPACE_DIR/install/setup.bash"
add_config_if_not_exist "if [ -r $LOCAL_SETUP_FILE ]; then source $LOCAL_SETUP_FILE; fi"

sleep 10
DISPLAY=:1 xfconf-query -c xfce4-desktop -p $(xfconf-query -c xfce4-desktop -l | grep "workspace0/last-image") -s /usr/share/backgrounds/xfce/lcas.jpg || true
DISPLAY=:1 xfconf-query -c xfce4-desktop -p $(xfconf-query -c xfce4-desktop -l | grep "workspace0/last-image") -s /usr/share/backgrounds/xfce/lcas.jpg || true
37 changes: 37 additions & 0 deletions .devcontainer/validate_fortress_runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Verify the non-interactive runtime contract required by the simulation launches.
set -eo pipefail

if [[ -n "${ROS_DISTRO:-}" && -r "/opt/ros/${ROS_DISTRO}/setup.bash" ]]; then
# shellcheck disable=SC1090
source "/opt/ros/${ROS_DISTRO}/setup.bash"
elif [[ -r /opt/ros/humble/setup.bash ]]; then
# shellcheck disable=SC1091
source /opt/ros/humble/setup.bash
else
echo 'ROS 2 setup.bash was not found; expected ROS 2 Humble.' >&2
exit 1
fi

set -u

ign_version="$(ign gazebo --version)"
if ! grep -Eq 'Gazebo[^0-9]*6(\.|$)' <<<"${ign_version}"; then
echo "Expected Gazebo Fortress (Gazebo Sim 6), got: ${ign_version}" >&2
exit 1
fi

for ros_package in ros_gz_sim ros_gz_bridge; do
ros2 pkg prefix "${ros_package}" >/dev/null
done

for system_plugin in \
'libignition-gazebo6-diff-drive-system.so*' \
'libignition-gazebo6-sensors-system.so*'; do
if ! find /usr/lib -type f -name "${system_plugin}" -print -quit | grep -q .; then
echo "Gazebo Fortress system plugin is not installed: ${system_plugin}" >&2
exit 1
fi
done

echo "Gazebo Fortress runtime verified: ${ign_version}"
16 changes: 14 additions & 2 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ This repository (LCAS/CMP9767) serves as the **image builder repository** for cr

```
├── src/ # Source packages to be included in the container
│ └── cmp9767_tutorial/ # Course-specific ROS2 packages
│ ├── cmp9767_tutorial/ # Course-specific ROS2 packages
│ ├── limo_description/ # Repository-owned Fortress robot description overlay
│ └── limo_gazebosim/ # Repository-owned Fortress simulation overlay
├── config/ # Configuration files
├── maps/ # Map files for navigation
├── params/ # Parameter files
Expand All @@ -22,6 +24,16 @@ This repository (LCAS/CMP9767) serves as the **image builder repository** for cr

## How It Works

### Gazebo Fortress ownership

The `lcas/limo_platform:2.2` base image includes the original LIMO packages,
which target Gazebo Classic. This repository intentionally supplies and builds
the `limo_description` and `limo_gazebosim` source overlays in `src/`; they are
the authoritative Gazebo Fortress versions for this image. Keep their ROS and
Gazebo dependencies in their package manifests. The Dockerfile relies on
`rosdep` and the pinned base image rather than installing broad ROS--Gazebo
metapackages directly.

### 1. Package Development Phase

**Add packages to the `src/` directory:**
Expand Down Expand Up @@ -234,4 +246,4 @@ To migrate from local Dockerfile builds to this system:
3. **Version control**: Tag this repository for stable releases
4. **Update documentation**: Point students to new template repository

This system provides a scalable, maintainable approach to managing development environments for robotics education, separating the complexity of image building from student development workflows.
This system provides a scalable, maintainable approach to managing development environments for robotics education, separating the complexity of image building from student development workflows.
53 changes: 35 additions & 18 deletions src/cmp9767_tutorial/cmp9767_tutorial/counter_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@
from std_msgs.msg import Header
from geometry_msgs.msg import PoseStamped, PoseArray

from cmp9767_tutorial.simulation_interfaces import SimulationInterfaces


class Counter3D(Node):
detection_threshold = 0.2 # in meters
detection_threshold = 0.2 # in meters

def __init__(self):
super().__init__("counter_3d")

def __init__(self):
super().__init__('counter_3d')

self.detected_objects = [] # list of all detected objects
self.interfaces = SimulationInterfaces.from_node(self)
self.detected_objects = [] # list of all detected objects

# subscribe to object detector
self.subscriber = self.create_subscription(PoseStamped, '/object_location',
self.counter_callback,
qos_profile=qos.qos_profile_sensor_data)

self.subscriber = self.create_subscription(
PoseStamped,
self.interfaces.topic("object_location"),
self.counter_callback,
qos_profile=qos.qos_profile_sensor_data,
)

# publish all detected object as an array of poses
self.publisher = self.create_publisher(PoseArray, '/object_count_array',
qos.qos_profile_parameters)
self.publisher = self.create_publisher(
PoseArray,
self.interfaces.topic("object_count_array"),
qos.qos_profile_parameters,
)

def counter_callback(self, data):
new_object = data.pose
Expand All @@ -32,25 +42,32 @@ def counter_callback(self, data):
# calculate the distance between the new_object and each in the list
pos_a = object.position
pos_b = new_object.position
d = math.sqrt((pos_a.x - pos_b.x) ** 2 + (pos_a.y - pos_b.y) ** 2 + (pos_a.z - pos_b.z) ** 2)
if d < self.detection_threshold: # found a close neighbour in the already existing list, so this one won't be added
d = math.sqrt(
(pos_a.x - pos_b.x) ** 2
+ (pos_a.y - pos_b.y) ** 2
+ (pos_a.z - pos_b.z) ** 2
)
if (
d < self.detection_threshold
): # found a close neighbour in the already existing list, so this one won't be added
object_exists = True
break

if not object_exists: # new object!
if not object_exists: # new object!
self.detected_objects.append(new_object)

# publish a PoseArray of object poses for visualisation in rviz
parray = PoseArray(header=Header(frame_id=data.header.frame_id))
for object in self.detected_objects:
parray.poses.append(object)
self.publisher.publish(parray)
self.publisher.publish(parray)

# print to the console
print(f'total count {len(self.detected_objects)}')
print(f"total count {len(self.detected_objects)}")
for object in self.detected_objects:
print(object.position)


def main(args=None):
rclpy.init(args=args)
counter_3d = Counter3D()
Expand All @@ -61,5 +78,5 @@ def main(args=None):
rclpy.shutdown()


if __name__ == '__main__':
main()
if __name__ == "__main__":
main()
31 changes: 21 additions & 10 deletions src/cmp9767_tutorial/cmp9767_tutorial/demo_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
import rclpy
from rclpy.parameter import Parameter

from cmp9767_tutorial.simulation_interfaces import SimulationInterfaces

"""
Basic stock inspection demo. In this demonstration, the expectation
Expand All @@ -29,7 +32,15 @@
def main():
rclpy.init()

navigator = BasicNavigator()
config_node = rclpy.create_node("demo_inspection_config")
interfaces = SimulationInterfaces.from_node(config_node)
config_node.declare_parameter("use_sim_time", True)
use_sim_time = config_node.get_parameter("use_sim_time").value
config_node.destroy_node()

navigator = BasicNavigator(namespace=interfaces.robot_namespace)
navigator.set_parameters([Parameter("use_sim_time", value=use_sim_time)])
map_frame = interfaces.frame("map")

# Inspection route, probably read in from a file for a real application
# from either a map or drive and repeat. (x, y, yaw)
Expand All @@ -44,7 +55,7 @@ def main():

# Set our demo's initial pose
initial_pose = PoseStamped()
initial_pose.header.frame_id = 'map'
initial_pose.header.frame_id = map_frame
initial_pose.header.stamp = navigator.get_clock().now().to_msg()
initial_pose.pose.position.x = 0.0
initial_pose.pose.position.y = 0.0
Expand All @@ -58,7 +69,7 @@ def main():
# Send our route
inspection_points = []
inspection_pose = PoseStamped()
inspection_pose.header.frame_id = 'map'
inspection_pose.header.frame_id = map_frame
inspection_pose.header.stamp = navigator.get_clock().now().to_msg()
for pt in inspection_route:
inspection_pose.pose.position.x = pt[0]
Expand All @@ -78,19 +89,19 @@ def main():
feedback = navigator.getFeedback()
if feedback and i % 5 == 0:
print(
'Executing current waypoint: '
"Executing current waypoint: "
+ str(feedback.current_waypoint + 1)
+ '/'
+ "/"
+ str(len(inspection_points))
)

result = navigator.getResult()
if result == TaskResult.SUCCEEDED:
print('Inspection of shelves complete! Returning to start...')
print("Inspection of shelves complete! Returning to start...")
elif result == TaskResult.CANCELED:
print('Inspection of shelving was canceled. Returning to start...')
print("Inspection of shelving was canceled. Returning to start...")
elif result == TaskResult.FAILED:
print('Inspection of shelving failed! Returning to start...')
print("Inspection of shelving failed! Returning to start...")

# go back to start
initial_pose.header.stamp = navigator.get_clock().now().to_msg()
Expand All @@ -101,5 +112,5 @@ def main():
exit(0)


if __name__ == '__main__':
main()
if __name__ == "__main__":
main()
Loading
Loading